pi-agent-browser-native 0.2.33 → 0.2.35

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 (44) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +47 -17
  3. package/docs/ARCHITECTURE.md +25 -13
  4. package/docs/COMMAND_REFERENCE.md +285 -47
  5. package/docs/ELECTRON.md +3 -3
  6. package/docs/RELEASE.md +22 -14
  7. package/docs/REQUIREMENTS.md +5 -5
  8. package/docs/SUPPORT_MATRIX.md +26 -22
  9. package/docs/TOOL_CONTRACT.md +97 -32
  10. package/extensions/agent-browser/index.ts +519 -2402
  11. package/extensions/agent-browser/lib/argv-descriptor.ts +90 -0
  12. package/extensions/agent-browser/lib/argv-grammar.ts +128 -0
  13. package/extensions/agent-browser/lib/command-policy.ts +71 -0
  14. package/extensions/agent-browser/lib/command-taxonomy.ts +336 -0
  15. package/extensions/agent-browser/lib/electron/cleanup.ts +1 -0
  16. package/extensions/agent-browser/lib/executable-path.ts +19 -0
  17. package/extensions/agent-browser/lib/input-modes/job.ts +62 -0
  18. package/extensions/agent-browser/lib/input-modes/params.ts +8 -8
  19. package/extensions/agent-browser/lib/input-modes.ts +3 -0
  20. package/extensions/agent-browser/lib/orchestration/batch-stdin.ts +65 -0
  21. package/extensions/agent-browser/lib/orchestration/browser-run/browser-action-model.ts +154 -0
  22. package/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.ts +149 -0
  23. package/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts +77 -29
  24. package/extensions/agent-browser/lib/orchestration/browser-run/final-result.ts +6 -2
  25. package/extensions/agent-browser/lib/orchestration/browser-run/index.ts +33 -27
  26. package/extensions/agent-browser/lib/orchestration/browser-run/prepare.ts +74 -23
  27. package/extensions/agent-browser/lib/orchestration/browser-run/process-output.ts +67 -17
  28. package/extensions/agent-browser/lib/orchestration/browser-run/prompt-guards.ts +93 -0
  29. package/extensions/agent-browser/lib/orchestration/browser-run/session-state.ts +19 -123
  30. package/extensions/agent-browser/lib/orchestration/browser-run/types.ts +32 -1
  31. package/extensions/agent-browser/lib/orchestration/electron-host/index.ts +860 -0
  32. package/extensions/agent-browser/lib/playbook.ts +24 -23
  33. package/extensions/agent-browser/lib/prompt-policy.ts +122 -0
  34. package/extensions/agent-browser/lib/results/action-recommendations.ts +3 -23
  35. package/extensions/agent-browser/lib/results/categories.ts +1 -1
  36. package/extensions/agent-browser/lib/results/presentation/navigation.ts +2 -34
  37. package/extensions/agent-browser/lib/results/presentation/registry.ts +34 -6
  38. package/extensions/agent-browser/lib/results/presentation/semantic-action.ts +133 -0
  39. package/extensions/agent-browser/lib/results/presentation.ts +11 -6
  40. package/extensions/agent-browser/lib/runtime.ts +93 -227
  41. package/extensions/agent-browser/lib/session-page-state.ts +31 -14
  42. package/extensions/agent-browser/lib/temp.ts +148 -23
  43. package/package.json +4 -4
  44. package/scripts/agent-browser-capability-baseline.mjs +198 -1
@@ -4,6 +4,7 @@
4
4
  * Scope: Presentation shaping only; upstream stdout parsing and snapshot compaction internals live in separate modules.
5
5
  */
6
6
 
7
+ import type { CompiledAgentBrowserSemanticAction } from "../input-modes/types.js";
7
8
  import { isRecord } from "../parsing.js";
8
9
  import type { CommandInfo } from "../runtime.js";
9
10
  import type { PersistentSessionArtifactStore } from "../temp.js";
@@ -43,6 +44,7 @@ import { buildErrorPresentation } from "./presentation/errors.js";
43
44
  import { compactLargePresentationOutput } from "./presentation/large-output.js";
44
45
  import { buildPageChangeSummary } from "./presentation/navigation.js";
45
46
  import { formatPresentationContentText, formatPresentationSummary } from "./presentation/registry.js";
47
+ import { resolvePresentationCommandInfo } from "./presentation/semantic-action.js";
46
48
 
47
49
  function sanitizeModelFacingPresentation(presentation: ToolPresentation): ToolPresentation {
48
50
  presentation.content = presentation.content.map((item) => {
@@ -65,6 +67,7 @@ export async function buildToolPresentation(options: {
65
67
  artifactRequest?: ArtifactRequestContext;
66
68
  batchArtifactRequests?: Array<ArtifactRequestContext | undefined>;
67
69
  commandInfo: CommandInfo;
70
+ compiledSemanticAction?: CompiledAgentBrowserSemanticAction;
68
71
  cwd: string;
69
72
  envelope?: AgentBrowserEnvelope;
70
73
  errorText?: string;
@@ -76,12 +79,14 @@ export async function buildToolPresentation(options: {
76
79
  artifactManifest,
77
80
  artifactRequest,
78
81
  commandInfo,
82
+ compiledSemanticAction,
79
83
  cwd,
80
84
  envelope,
81
85
  errorText,
82
86
  persistentArtifactStore,
83
87
  sessionName,
84
88
  } = options;
89
+ const presentationCommandInfo = resolvePresentationCommandInfo(commandInfo, compiledSemanticAction);
85
90
 
86
91
  if (errorText) {
87
92
  return buildErrorPresentation({ args, commandInfo, errorText, sessionName });
@@ -89,10 +94,10 @@ export async function buildToolPresentation(options: {
89
94
 
90
95
  const data = enrichStreamStatusData(commandInfo, envelope?.data);
91
96
  const presentationData = redactPresentationData(commandInfo, data);
92
- const artifacts = await extractFileArtifacts({ artifactRequest, commandInfo, cwd, data, sessionName });
97
+ const artifacts = await extractFileArtifacts({ artifactRequest, commandInfo: presentationCommandInfo, cwd, data, sessionName });
93
98
  const artifactVerification = buildArtifactVerificationSummary(artifacts);
94
99
  const artifactSummary = formatArtifactSummary(artifacts);
95
- const summary = artifactSummary ?? formatPresentationSummary(commandInfo, data);
100
+ const summary = artifactSummary ?? formatPresentationSummary(commandInfo, data, compiledSemanticAction);
96
101
  const artifactText = artifacts.length > 0 ? formatArtifactMetadataLines(artifacts).join("\n") : undefined;
97
102
 
98
103
  let presentation: ToolPresentation;
@@ -113,7 +118,7 @@ export async function buildToolPresentation(options: {
113
118
  presentation = {
114
119
  artifactVerification,
115
120
  artifacts: artifacts.length > 0 ? artifacts : undefined,
116
- content: [{ type: "text", text: artifactText ?? formatPresentationContentText(commandInfo, data) }],
121
+ content: [{ type: "text", text: artifactText ?? formatPresentationContentText(commandInfo, data, compiledSemanticAction) }],
117
122
  data: presentationData,
118
123
  summary,
119
124
  };
@@ -159,7 +164,7 @@ export async function buildToolPresentation(options: {
159
164
  if (!presentationWithManifest.resultCategory) {
160
165
  const categoryDetails = buildAgentBrowserResultCategoryDetails({
161
166
  artifacts: presentationWithManifest.artifacts,
162
- command: commandInfo.command,
167
+ command: presentationCommandInfo.command,
163
168
  confirmationRequired: confirmationRequired !== undefined,
164
169
  errorText: envelope?.success === false ? presentationWithManifest.summary : undefined,
165
170
  savedFile: presentationWithManifest.savedFile,
@@ -186,7 +191,7 @@ export async function buildToolPresentation(options: {
186
191
  const genericNextActions = presentationWithManifest.nextActions ? undefined : buildAgentBrowserNextActions({
187
192
  artifacts: presentationWithManifest.artifacts,
188
193
  args,
189
- command: commandInfo.command,
194
+ command: presentationCommandInfo.command,
190
195
  confirmationId: confirmationRequired?.id,
191
196
  failureCategory: presentationWithManifest.failureCategory,
192
197
  resultCategory: presentationWithManifest.resultCategory ?? "success",
@@ -203,7 +208,7 @@ export async function buildToolPresentation(options: {
203
208
  );
204
209
  presentationWithManifest.pageChangeSummary = presentationWithManifest.pageChangeSummary ?? buildPageChangeSummary({
205
210
  artifacts: presentationWithManifest.artifacts,
206
- commandInfo,
211
+ commandInfo: presentationCommandInfo,
207
212
  data,
208
213
  nextActions: presentationWithManifest.nextActions,
209
214
  savedFilePath: presentationWithManifest.savedFilePath,
@@ -11,6 +11,23 @@
11
11
  import { createHash, randomUUID } from "node:crypto";
12
12
  import { basename } from "node:path";
13
13
 
14
+ import {
15
+ extractCommandTokens,
16
+ findCommandStartIndex,
17
+ parseArgvDescriptor,
18
+ parseCommandInfo,
19
+ type CommandInfo,
20
+ } from "./argv-descriptor.js";
21
+ import {
22
+ GLOBAL_VALUE_FLAGS_ALLOWING_DASH_VALUE,
23
+ PREVALIDATED_VALUE_FLAGS,
24
+ } from "./argv-grammar.js";
25
+ import { needsManagedSession } from "./command-policy.js";
26
+ import { isCloseCommand, isOpenNavigationCommand } from "./command-taxonomy.js";
27
+
28
+ export type { CommandInfo } from "./argv-descriptor.js";
29
+ export { extractCommandTokens, findCommandStartIndex, parseArgvDescriptor, parseCommandInfo } from "./argv-descriptor.js";
30
+
14
31
  import { isRecord } from "./parsing.js";
15
32
 
16
33
  /**
@@ -81,7 +98,6 @@ const LAUNCH_SCOPED_FLAG_LABEL = LAUNCH_SCOPED_FLAG_DEFINITIONS.map((definition)
81
98
  * tab-correction (the `tab list` + re-select cycle).
82
99
  */
83
100
  const LAUNCH_SCOPED_TAB_CORRECTION_FLAGS = new Set(["--profile", "--session-name", "--state"] as const);
84
- const OPEN_COMMANDS = new Set(["goto", "navigate", "open"]);
85
101
  const OPENAI_HEADLESS_COMPAT_HOSTS = new Set(["chat.com", "chat.openai.com", "chatgpt.com"]);
86
102
  const BRAVE_API_KEY_ENV = "BRAVE_API_KEY";
87
103
  const AGENT_BROWSER_IDLE_TIMEOUT_ENV = "AGENT_BROWSER_IDLE_TIMEOUT_MS";
@@ -89,99 +105,13 @@ const IMPLICIT_SESSION_IDLE_TIMEOUT_ENV = "PI_AGENT_BROWSER_IMPLICIT_SESSION_IDL
89
105
  const IMPLICIT_SESSION_CLOSE_TIMEOUT_ENV = "PI_AGENT_BROWSER_IMPLICIT_SESSION_CLOSE_TIMEOUT_MS";
90
106
  const DEFAULT_IMPLICIT_SESSION_IDLE_TIMEOUT_MS = 15 * 60 * 1000;
91
107
  const DEFAULT_IMPLICIT_SESSION_CLOSE_TIMEOUT_MS = 5_000;
92
- const LEGACY_BASH_ALLOW_PATTERNS = [
93
- /\b(?:bash-oriented workflow|bash workflow)\b/i,
94
- /\b(?:use|via|through|with)\s+bash\b/i,
95
- /\bnpx\s+agent-browser\b/i,
96
- /\bagent-browser\s+--(?:help|version)\b/i,
97
- /\bdebug(?:ging)?\b.*\b(?:agent[_ -]?browser|agent_browser|browser integration)\b/i,
98
- ];
99
- const BROWSER_PROMPT_PATTERNS = [
100
- /\b(?:agent[_ -]?browser|browser automation|eval\s+--stdin|screenshot|snapshot|tab\s+list)\b/i,
101
- /\b(?:react\s+(?:tree|inspect|renders|suspense)|web\s+vitals|core\s+web\s+vitals|pushstate)\b/i,
102
- /\b(?:live\s+docs?|online\s+research|research\s+(?:online|the\s+web)|search\s+(?:online|the\s+web)|web\s+research)\b/i,
103
- /\bbrowser\b.*\b(?:automation|click|fill|navigate|open|page|screenshot|site|snapshot|tab|url|visit|web(?:site| page)?)\b/i,
104
- /\b(?:browse|click|fill|login|navigate|open|visit)\b.*\b(?:https?:\/\/\S+|page|site|tab|url|web(?:site| page)?)\b/i,
105
- ];
106
108
  const INSPECTION_FLAGS = new Set(["--help", "-h", "--version", "-V"]);
107
109
  const SENSITIVE_VALUE_FLAGS = new Set(["--body", "--headers", "--password", "--proxy"]);
108
- const GLOBAL_VALUE_FLAGS_ALLOWING_DASH_VALUE = new Set(["--args"]);
109
- const GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES = new Set([
110
- "--allow-file-access",
111
- "--annotate",
112
- "--auto-connect",
113
- "--confirm-interactive",
114
- "--content-boundaries",
115
- "--debug",
116
- "--headed",
117
- "--ignore-https-errors",
118
- "--json",
119
- "--no-auto-dialog",
120
- "--quiet",
121
- "-q",
122
- "--verbose",
123
- "-v",
124
- ]);
125
110
  const SENSITIVE_QUERY_PARAM_PATTERN =
126
111
  /^(?:access(?:_|-)?token|api(?:_|-)?key|auth|authorization|bearer|client(?:_|-)?secret|code|cookie|id(?:_|-)?token|key|pass(?:word)?|refresh(?:_|-)?token|secret|sentry(?:_|-)?key|session(?:_|-)?id|sig(?:nature)?|token|write(?:_|-)?key)$/i;
127
112
  const SENSITIVE_FIELD_NAME_PATTERN =
128
113
  /^(?:access(?:_|-)?token|api(?:_|-)?key|auth(?:orization)?|bearer|client(?:_|-)?secret|cookie|id(?:_|-)?token|pass(?:word)?|proxy(?:_|-)?authorization|refresh(?:_|-)?token|secret|sentry(?:_|-)?key|session(?:_|-)?id|set(?:_|-)?cookie|sig(?:nature)?|token|write(?:_|-)?key|x(?:_|-)?api(?:_|-)?key)$/i;
129
114
 
130
- const VALUE_FLAGS = new Set([
131
- "--session",
132
- "--cdp",
133
- "--config",
134
- "--profile",
135
- "--session-name",
136
- "--proxy",
137
- "--proxy-bypass",
138
- "--headers",
139
- "--executable-path",
140
- "--extension",
141
- "--init-script",
142
- "--enable",
143
- "--provider",
144
- "-p",
145
- "--engine",
146
- "--state",
147
- "--download-path",
148
- "--screenshot-dir",
149
- "--screenshot-format",
150
- "--screenshot-quality",
151
- "--color-scheme",
152
- "--device",
153
- "--port",
154
- "--args",
155
- "--user-agent",
156
- "--allowed-domains",
157
- "--action-policy",
158
- "--confirm-actions",
159
- "--max-output",
160
- "--model",
161
- "--baseline",
162
- "--body",
163
- "--categories",
164
- "--curl",
165
- "--depth",
166
- "-d",
167
- "--domain",
168
- "--expires",
169
- "--filter",
170
- "--fn",
171
- "--label",
172
- "--load",
173
- "--name",
174
- "--path",
175
- "--resource-type",
176
- "--sameSite",
177
- "--selector",
178
- "-s",
179
- "--text",
180
- "--timeout",
181
- "--url",
182
- "--username",
183
- "--password",
184
- ]);
185
115
  const DEFAULT_HEADLESS_COMPAT_USER_AGENT_BY_PLATFORM: Partial<Record<NodeJS.Platform, string>> = {
186
116
  darwin: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
187
117
  linux: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
@@ -194,11 +124,6 @@ const MAX_PROJECT_SLUG_LENGTH = 24;
194
124
  const SESSION_NAME_CWD_HASH_LENGTH = 8;
195
125
  const SESSION_NAME_SESSION_ID_LENGTH = 12;
196
126
 
197
- export interface CommandInfo {
198
- command?: string;
199
- subcommand?: string;
200
- }
201
-
202
127
  export type SessionMode = "auto" | "fresh";
203
128
 
204
129
  export interface SessionRecoveryHint {
@@ -248,13 +173,10 @@ export interface ManagedSessionState {
248
173
  }
249
174
 
250
175
  export interface RestoredManagedSessionState extends ManagedSessionState {
176
+ closedSessionName?: string;
251
177
  freshSessionOrdinal: number;
252
178
  }
253
179
 
254
- export interface PromptPolicy {
255
- allowLegacyAgentBrowserBash: boolean;
256
- }
257
-
258
180
  function isStringArray(value: unknown): value is string[] {
259
181
  return Array.isArray(value) && value.every((item) => typeof item === "string");
260
182
  }
@@ -504,22 +426,10 @@ export function redactInvocationArgs(args: string[]): string[] {
504
426
  return redacted;
505
427
  }
506
428
 
507
- export function shouldAppendBrowserSystemPrompt(prompt: string): boolean {
508
- const normalizedPrompt = prompt.trim();
509
- if (normalizedPrompt.length === 0) {
510
- return false;
511
- }
512
- return BROWSER_PROMPT_PATTERNS.some((pattern) => pattern.test(normalizedPrompt));
513
- }
514
-
515
429
  export function isPlainTextInspectionArgs(args: string[]): boolean {
516
430
  return args.some((token) => INSPECTION_FLAGS.has(token));
517
431
  }
518
432
 
519
- function isStatelessInspectionCommand(commandInfo: CommandInfo): boolean {
520
- return commandInfo.command === "skills" && ["list", "get", "path"].includes(commandInfo.subcommand ?? "");
521
- }
522
-
523
433
  export function hasUsableBraveApiKey(apiKey: string | null | undefined = process.env[BRAVE_API_KEY_ENV]): boolean {
524
434
  return typeof apiKey === "string" && apiKey.trim().length > 0;
525
435
  }
@@ -556,7 +466,7 @@ export function resolveManagedSessionState(options: {
556
466
  if (!managedSessionName) {
557
467
  return { active: priorActive, sessionName: priorSessionName };
558
468
  }
559
- if (command === "close" && managedSessionName === priorSessionName) {
469
+ if (isCloseCommand(command) && managedSessionName === priorSessionName) {
560
470
  return { active: succeeded ? false : priorActive, sessionName: priorSessionName };
561
471
  }
562
472
  if (!succeeded) {
@@ -594,6 +504,29 @@ function getManagedSessionRestoreRank(options: {
594
504
  return nextRank;
595
505
  }
596
506
 
507
+ function getRestorableManagedSessionName(value: unknown, fallbackSessionName: string): string | undefined {
508
+ return typeof value === "string" && isRestorableManagedSessionName(value, fallbackSessionName) ? value : undefined;
509
+ }
510
+
511
+ function getElectronCleanupClosedManagedSessionNames(details: Record<string, unknown>, fallbackSessionName: string): string[] {
512
+ const electron = isRecord(details.electron) ? details.electron : undefined;
513
+ const cleanup = isRecord(electron?.cleanup) ? electron.cleanup : undefined;
514
+ const results = Array.isArray(cleanup?.results) ? cleanup.results : [];
515
+ const closedSessionNames = new Set<string>();
516
+ for (const result of results) {
517
+ if (!isRecord(result) || !Array.isArray(result.steps)) continue;
518
+ const record = isRecord(result.record) ? result.record : undefined;
519
+ for (const step of result.steps) {
520
+ if (!isRecord(step) || step.resource !== "managed-session") continue;
521
+ if (step.state !== "removed" && step.state !== "already-gone") continue;
522
+ const sessionName = getRestorableManagedSessionName(step.sessionName, fallbackSessionName)
523
+ ?? getRestorableManagedSessionName(record?.sessionName, fallbackSessionName);
524
+ if (sessionName) closedSessionNames.add(sessionName);
525
+ }
526
+ }
527
+ return [...closedSessionNames];
528
+ }
529
+
597
530
  export function restoreManagedSessionStateFromBranch(
598
531
  branch: unknown[],
599
532
  fallbackSessionName: string,
@@ -603,9 +536,21 @@ export function restoreManagedSessionStateFromBranch(
603
536
  sessionName: fallbackSessionName,
604
537
  };
605
538
  let activeRestoreRank = 0;
539
+ let closedSessionName: string | undefined;
606
540
  let freshSessionOrdinal = 0;
607
541
  const freshSessionRanks = new Map<string, number>();
608
542
 
543
+ const applyManagedClose = (sessionName: string): void => {
544
+ const restoreRank = getManagedSessionRestoreRank({
545
+ fallbackSessionName,
546
+ freshSessionRanks,
547
+ sessionName,
548
+ });
549
+ if (restoreRank === undefined || sessionName !== restoredState.sessionName) return;
550
+ restoredState = { active: false, sessionName: restoredState.sessionName };
551
+ closedSessionName = sessionName;
552
+ };
553
+
609
554
  for (const entry of branch) {
610
555
  if (!isRecord(entry) || entry.type !== "message") {
611
556
  continue;
@@ -623,17 +568,35 @@ export function restoreManagedSessionStateFromBranch(
623
568
  continue;
624
569
  }
625
570
 
571
+ for (const sessionName of getElectronCleanupClosedManagedSessionNames(details, fallbackSessionName)) {
572
+ applyManagedClose(sessionName);
573
+ }
574
+
626
575
  const explicitSessionName = extractExplicitSessionName(args);
627
576
  const sessionName = typeof details.sessionName === "string" ? details.sessionName : undefined;
628
577
  const sessionMode = details.sessionMode === "fresh" || details.sessionMode === "auto" ? details.sessionMode : undefined;
629
578
  const usedImplicitSession = details.usedImplicitSession === true;
579
+ const command = typeof details.command === "string" ? details.command : parseCommandInfo(args).command;
580
+ const commandClosesSession = isCloseCommand(command);
581
+ const outcome = typeof details.managedSessionOutcome === "object" && details.managedSessionOutcome !== null ? details.managedSessionOutcome as Record<string, unknown> : undefined;
582
+ const outcomeStatus = typeof outcome?.status === "string" ? outcome.status : undefined;
583
+ const outcomeCurrentSessionName = typeof outcome?.currentSessionName === "string" ? outcome.currentSessionName : undefined;
584
+ const outcomeAttemptedSessionName = getRestorableManagedSessionName(outcome?.attemptedSessionName, fallbackSessionName);
585
+ const outcomeClosedSessionName = outcomeStatus === "closed" && outcome?.succeeded === true
586
+ ? outcomeAttemptedSessionName ?? getRestorableManagedSessionName(outcomeCurrentSessionName, fallbackSessionName) ?? getRestorableManagedSessionName(sessionName, fallbackSessionName)
587
+ : undefined;
588
+ const restorableDetailSessionName = getRestorableManagedSessionName(sessionName, fallbackSessionName);
589
+ const explicitCloseSessionName = commandClosesSession && explicitSessionName && restorableDetailSessionName === explicitSessionName
590
+ ? restorableDetailSessionName
591
+ : undefined;
630
592
  const managedSessionName =
631
593
  !explicitSessionName &&
632
- sessionName &&
633
- isRestorableManagedSessionName(sessionName, fallbackSessionName) &&
594
+ restorableDetailSessionName &&
634
595
  (usedImplicitSession || sessionMode === "fresh")
635
- ? sessionName
636
- : undefined;
596
+ ? restorableDetailSessionName
597
+ : commandClosesSession
598
+ ? outcomeClosedSessionName ?? explicitCloseSessionName
599
+ : undefined;
637
600
  if (!managedSessionName) {
638
601
  continue;
639
602
  }
@@ -646,25 +609,19 @@ export function restoreManagedSessionStateFromBranch(
646
609
  if (restoreRank === undefined) {
647
610
  continue;
648
611
  }
612
+ freshSessionOrdinal = Math.max(freshSessionOrdinal, restoreRank);
649
613
 
650
614
  const messageIsError = typeof message.isError === "boolean" ? message.isError : undefined;
651
615
  const exitCode = typeof details.exitCode === "number" ? details.exitCode : undefined;
652
- const outcome = typeof details.managedSessionOutcome === "object" && details.managedSessionOutcome !== null ? details.managedSessionOutcome as Record<string, unknown> : undefined;
653
- const outcomeStatus = typeof outcome?.status === "string" ? outcome.status : undefined;
654
- const outcomeCurrentSessionName = typeof outcome?.currentSessionName === "string" ? outcome.currentSessionName : undefined;
655
616
  const outcomeActiveAfter = outcome?.activeAfter === true;
656
617
  const outcomeRepresentsActiveCurrentSession = outcomeActiveAfter && outcomeCurrentSessionName === managedSessionName && (outcomeStatus === "created" || outcomeStatus === "replaced" || outcomeStatus === "unchanged");
657
618
  const succeeded = outcomeRepresentsActiveCurrentSession ? true : messageIsError === undefined ? exitCode === undefined || exitCode === 0 : !messageIsError;
658
- const command = typeof details.command === "string" ? details.command : parseCommandInfo(args).command;
659
- if ((succeeded || outcomeRepresentsActiveCurrentSession) && sessionMode === "fresh") {
660
- freshSessionOrdinal += 1;
661
- }
662
- const staleCompletion = succeeded && command !== "close" && restoreRank < activeRestoreRank;
663
- if (staleCompletion) {
619
+ if (commandClosesSession) {
620
+ if (succeeded) applyManagedClose(managedSessionName);
664
621
  continue;
665
622
  }
666
- const staleClose = command === "close" && restoredState.active && managedSessionName !== restoredState.sessionName;
667
- if (staleClose) {
623
+ const staleCompletion = succeeded && restoreRank < activeRestoreRank;
624
+ if (staleCompletion) {
668
625
  continue;
669
626
  }
670
627
 
@@ -675,13 +632,15 @@ export function restoreManagedSessionStateFromBranch(
675
632
  priorSessionName: restoredState.sessionName,
676
633
  succeeded,
677
634
  });
678
- if (succeeded && command !== "close" && restoredState.active) {
635
+ if (succeeded && restoredState.active) {
679
636
  activeRestoreRank = restoreRank;
637
+ closedSessionName = undefined;
680
638
  }
681
639
  }
682
640
 
683
641
  return {
684
642
  ...restoredState,
643
+ ...(closedSessionName ? { closedSessionName } : {}),
685
644
  freshSessionOrdinal,
686
645
  };
687
646
  }
@@ -739,11 +698,6 @@ export function validateToolArgs(args: string[]): string | undefined {
739
698
  return undefined;
740
699
  }
741
700
 
742
- function isBooleanLiteral(token: string | undefined): boolean {
743
- const normalized = token?.trim().toLowerCase();
744
- return normalized === "true" || normalized === "false";
745
- }
746
-
747
701
  function getInvalidValueFlagDetails(args: string[]): InvalidValueFlagDetails | undefined {
748
702
  for (let index = 0; index < args.length; index += 1) {
749
703
  const token = args[index];
@@ -751,7 +705,7 @@ function getInvalidValueFlagDetails(args: string[]): InvalidValueFlagDetails | u
751
705
  continue;
752
706
  }
753
707
  const normalizedToken = token.split("=", 1)[0] ?? token;
754
- if (!VALUE_FLAGS.has(normalizedToken)) {
708
+ if (!PREVALIDATED_VALUE_FLAGS.has(normalizedToken)) {
755
709
  continue;
756
710
  }
757
711
  if (token.includes("=")) {
@@ -876,7 +830,7 @@ function getDefaultHeadlessCompatUserAgent(platform: NodeJS.Platform = process.p
876
830
  }
877
831
 
878
832
  function getCompatibilityWorkaround(args: string[], commandInfo: CommandInfo): CompatibilityWorkaround | undefined {
879
- if (!commandInfo.command || !OPEN_COMMANDS.has(commandInfo.command) || !commandInfo.subcommand) {
833
+ if (!commandInfo.command || !isOpenNavigationCommand(commandInfo.command) || !commandInfo.subcommand) {
880
834
  return undefined;
881
835
  }
882
836
  if (hasFlagToken(args, "--user-agent")) {
@@ -947,40 +901,6 @@ export function hasLaunchScopedTabCorrectionFlag(args: string[]): boolean {
947
901
  });
948
902
  }
949
903
 
950
- export function buildPromptPolicy(prompt: string): PromptPolicy {
951
- return {
952
- allowLegacyAgentBrowserBash: LEGACY_BASH_ALLOW_PATTERNS.some((pattern) => pattern.test(prompt)),
953
- };
954
- }
955
-
956
- function getMessageText(content: unknown): string {
957
- if (typeof content === "string") return content;
958
- if (!Array.isArray(content)) return "";
959
-
960
- return content
961
- .map((item) => {
962
- if (typeof item !== "object" || item === null) return "";
963
- return item.type === "text" && typeof item.text === "string" ? item.text : "";
964
- })
965
- .filter((text) => text.length > 0)
966
- .join("\n");
967
- }
968
-
969
- export function getLatestUserPrompt(branch: unknown[]): string {
970
- for (let index = branch.length - 1; index >= 0; index -= 1) {
971
- const entry = branch[index];
972
- if (typeof entry !== "object" || entry === null || !("type" in entry) || entry.type !== "message") {
973
- continue;
974
- }
975
- const message = "message" in entry ? entry.message : undefined;
976
- if (typeof message !== "object" || message === null || !("role" in message) || message.role !== "user") {
977
- continue;
978
- }
979
- return getMessageText("content" in message ? message.content : undefined);
980
- }
981
- return "";
982
- }
983
-
984
904
  export function buildExecutionPlan(
985
905
  args: string[],
986
906
  options: {
@@ -993,8 +913,10 @@ export function buildExecutionPlan(
993
913
  const invalidValueFlag = getInvalidValueFlagDetails(args);
994
914
  const startupScopedFlags = getStartupScopedFlags(args);
995
915
  const plainTextInspection = isPlainTextInspectionArgs(args);
996
- const commandInfo = parseCommandInfo(args);
997
- const statelessInspection = plainTextInspection || isStatelessInspectionCommand(commandInfo);
916
+ const argvDescriptor = parseArgvDescriptor(args);
917
+ const commandTokens = argvDescriptor.commandTokens;
918
+ const commandInfo = argvDescriptor.commandInfo;
919
+ const commandNeedsManagedSession = !plainTextInspection && needsManagedSession(argvDescriptor);
998
920
  const effectiveArgs = plainTextInspection ? [...args] : args.includes("--json") ? [] : ["--json"];
999
921
  if (invalidValueFlag) {
1000
922
  return {
@@ -1020,7 +942,7 @@ export function buildExecutionPlan(
1020
942
 
1021
943
  const explicitSessionName = extractExplicitSessionName(args);
1022
944
  const shouldCreateFreshManagedSession =
1023
- !explicitSessionName && options.sessionMode === "fresh" && commandInfo.command !== undefined && commandInfo.command !== "close";
945
+ !explicitSessionName && options.sessionMode === "fresh" && commandInfo.command !== undefined && !isCloseCommand(commandInfo.command);
1024
946
  const compatibilityWorkaround = getCompatibilityWorkaround(args, commandInfo);
1025
947
  let managedSessionName: string | undefined;
1026
948
  let recoveryHint: SessionRecoveryHint | undefined;
@@ -1028,7 +950,7 @@ export function buildExecutionPlan(
1028
950
  let usedImplicitSession = false;
1029
951
  let validationError: string | undefined;
1030
952
 
1031
- if (!explicitSessionName && options.sessionMode === "auto" && !statelessInspection) {
953
+ if (!explicitSessionName && options.sessionMode === "auto" && commandNeedsManagedSession) {
1032
954
  if (options.managedSessionActive && startupScopedFlags.length > 0) {
1033
955
  recoveryHint = {
1034
956
  exampleArgs: args,
@@ -1047,7 +969,7 @@ export function buildExecutionPlan(
1047
969
  sessionName = options.managedSessionName;
1048
970
  usedImplicitSession = true;
1049
971
  }
1050
- } else if (shouldCreateFreshManagedSession && !statelessInspection) {
972
+ } else if (shouldCreateFreshManagedSession && commandNeedsManagedSession) {
1051
973
  effectiveArgs.push("--session", options.freshSessionName);
1052
974
  managedSessionName = options.freshSessionName;
1053
975
  sessionName = options.freshSessionName;
@@ -1116,59 +1038,3 @@ export function chooseOpenResultTabCorrection(options: {
1116
1038
  }
1117
1039
  : undefined;
1118
1040
  }
1119
-
1120
- function getOpenCommandTarget(commandTokens: string[]): string | undefined {
1121
- for (let index = 1; index < commandTokens.length; index += 1) {
1122
- const token = commandTokens[index];
1123
- if (token === "--init-script" || token === "--enable") {
1124
- index += 1;
1125
- continue;
1126
- }
1127
- if (token.startsWith("--init-script=") || token.startsWith("--enable=")) {
1128
- continue;
1129
- }
1130
- if (token.startsWith("-")) {
1131
- continue;
1132
- }
1133
- return token;
1134
- }
1135
- return undefined;
1136
- }
1137
-
1138
- export function parseCommandInfo(args: string[]): CommandInfo {
1139
- const commandTokens = extractCommandTokens(args);
1140
- const command = commandTokens[0];
1141
- return {
1142
- command,
1143
- subcommand: command && OPEN_COMMANDS.has(command) ? getOpenCommandTarget(commandTokens) : commandTokens[1],
1144
- };
1145
- }
1146
-
1147
- function findCommandStartIndex(args: string[]): number | undefined {
1148
- for (let index = 0; index < args.length; index += 1) {
1149
- const token = args[index];
1150
- if (token.startsWith("--session=")) {
1151
- continue;
1152
- }
1153
- if (token.startsWith("-")) {
1154
- const normalizedToken = token.split("=", 1)[0] ?? token;
1155
- if (VALUE_FLAGS.has(normalizedToken) && !token.includes("=")) {
1156
- index += 1;
1157
- } else if (
1158
- GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES.has(normalizedToken) &&
1159
- !token.includes("=") &&
1160
- isBooleanLiteral(args[index + 1])
1161
- ) {
1162
- index += 1;
1163
- }
1164
- continue;
1165
- }
1166
- return index;
1167
- }
1168
- return undefined;
1169
- }
1170
-
1171
- export function extractCommandTokens(args: string[]): string[] {
1172
- const commandStartIndex = findCommandStartIndex(args);
1173
- return commandStartIndex === undefined ? [] : args.slice(commandStartIndex);
1174
- }