pi-agent-browser-native 0.2.12 → 0.2.14

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.
@@ -6,11 +6,16 @@
6
6
  * Invariants/Assumptions: agent-browser is installed separately on PATH, the wrapper targets the current locally installed upstream version only, and no backward-compatibility shims are provided.
7
7
  */
8
8
 
9
- import { rm } from "node:fs/promises";
9
+ import { readFile, rm } from "node:fs/promises";
10
10
 
11
- import { isToolCallEventType, type ExtensionAPI } from "@mariozechner/pi-coding-agent";
11
+ import { StringEnum } from "@mariozechner/pi-ai";
12
+ import { isToolCallEventType, type AgentToolResult, type ExtensionAPI } from "@mariozechner/pi-coding-agent";
12
13
  import { Type } from "typebox";
13
14
 
15
+ import {
16
+ PROJECT_RULE_PROMPT,
17
+ buildToolPromptGuidelines,
18
+ } from "./lib/playbook.js";
14
19
  import { runAgentBrowserProcess } from "./lib/process.js";
15
20
  import {
16
21
  buildToolPresentation,
@@ -30,7 +35,9 @@ import {
30
35
  getImplicitSessionCloseTimeoutMs,
31
36
  getImplicitSessionIdleTimeoutMs,
32
37
  getLatestUserPrompt,
38
+ hasLaunchScopedTabCorrectionFlag,
33
39
  hasUsableBraveApiKey,
40
+ extractExplicitSessionName,
34
41
  redactInvocationArgs,
35
42
  redactSensitiveText,
36
43
  redactSensitiveValue,
@@ -41,7 +48,20 @@ import {
41
48
  type CompatibilityWorkaround,
42
49
  type OpenResultTabCorrection,
43
50
  } from "./lib/runtime.js";
44
- import { cleanupSecureTempArtifacts, type PersistentSessionArtifactStore } from "./lib/temp.js";
51
+ import {
52
+ cleanupSecureTempArtifacts,
53
+ type PersistentSessionArtifactEviction,
54
+ type PersistentSessionArtifactStore,
55
+ writePersistentSessionArtifactFile,
56
+ writeSecureTempFile,
57
+ } from "./lib/temp.js";
58
+ import {
59
+ type SessionArtifactManifest,
60
+ buildEvictedSessionArtifactEntries,
61
+ formatSessionArtifactRetentionSummary,
62
+ isSessionArtifactManifest,
63
+ mergeSessionArtifactManifest,
64
+ } from "./lib/results/shared.js";
45
65
 
46
66
  const DEFAULT_SESSION_MODE = "auto" as const;
47
67
 
@@ -50,54 +70,20 @@ const AGENT_BROWSER_PARAMS = Type.Object({
50
70
  description: "Exact agent-browser CLI arguments, excluding the binary name and any shell operators.",
51
71
  minItems: 1,
52
72
  }),
53
- stdin: Type.Optional(Type.String({ description: "Optional raw stdin content for commands like eval --stdin or batch." })),
73
+ stdin: Type.Optional(Type.String({ description: "Optional raw stdin content; only supported for batch and eval --stdin." })),
54
74
  sessionMode: Type.Optional(
55
- Type.Union([Type.Literal("auto"), Type.Literal("fresh")], {
75
+ StringEnum(["auto", "fresh"] as const, {
56
76
  description:
57
- "Session handling mode. `auto` reuses the extension-managed pi-scoped session when possible. `fresh` switches that managed session to a fresh upstream launch so startup-scoped flags like --profile, --session-name, or --cdp apply and later auto calls follow the new browser.",
77
+ "Session handling mode. `auto` reuses the extension-managed pi-scoped session when possible. `fresh` switches that managed session to a fresh upstream launch so launch-scoped flags like --profile, --session-name, --cdp, --state, or --auto-connect apply and later auto calls follow the new browser.",
58
78
  default: DEFAULT_SESSION_MODE,
59
79
  }),
60
80
  ),
61
81
  });
62
- const PROJECT_RULE_PROMPT =
63
- "Project rule: when browser automation is needed, prefer the native `agent_browser` tool. Do not run direct `agent-browser` bash commands unless the user explicitly asks for a bash-oriented workflow or browser-integration debugging.";
64
- const QUICK_START_GUIDELINES = [
65
- "Quick start mental model: args are the exact agent-browser CLI args after the binary; stdin is only for batch and eval --stdin; sessionMode=fresh switches the extension-managed session to a fresh upstream launch when you need new --profile, --session-name, or --cdp state.",
66
- "Common first calls: { args: [\"open\", \"https://example.com\"] } then { args: [\"snapshot\", \"-i\"] }; after navigation, use { args: [\"click\", \"@e2\"] } then { args: [\"snapshot\", \"-i\"] }.",
67
- "Common advanced calls: { args: [\"batch\"], stdin: \"[[\\\"open\\\",\\\"https://example.com\\\"],[\\\"snapshot\\\",\\\"-i\\\"]]\" }, { args: [\"eval\", \"--stdin\"], stdin: \"document.title\" }, and { args: [\"--profile\", \"Default\", \"open\", \"https://example.com/account\"], sessionMode: \"fresh\" }.",
68
- "High-value command reference: download <selector> <path> saves a file triggered by a click; get title/url/text/html/value/attr/count reads page state; screenshot [path] captures an image; pdf <path> saves a PDF; tab list and tab <tab-id-or-label> inspect or recover the active tab.",
69
- ] as const;
70
- const BRAVE_SEARCH_PROMPT_GUIDELINE =
71
- "When a non-empty BRAVE_API_KEY is available in the current environment, prefer the Brave Search API via bash/curl to discover specific destination URLs, then open the chosen URL with agent_browser instead of browsing a search engine results page just to find the target.";
72
- const SHARED_BROWSER_PLAYBOOK_GUIDELINES = [
73
- "Standard workflow: open the page, snapshot -i, interact using refs, and re-snapshot after navigation or major DOM changes.",
74
- "For authenticated or user-specific content like feeds, inboxes, dashboards, and accounts, prefer --profile Default on the first browser call and let the implicit session carry continuity. Use --auto-connect only if profile-based reuse is unavailable or the task is specifically about attaching to a running debug-enabled browser.",
75
- "Do not invent fixed explicit session names for routine tasks. Use the implicit session unless you truly need multiple isolated browser sessions in the same conversation.",
76
- "When using --profile, --session-name, or --cdp, put them on the first command for that session. If you intentionally use an explicit --session, keep using that same explicit session for follow-ups.",
77
- "If you already used the implicit session and now need startup-scoped flags like --profile, --session-name, or --cdp, retry with sessionMode set to fresh or pass an explicit --session for the new launch. After a successful unnamed fresh launch, later auto calls follow that new session.",
78
- "If a session lands on the wrong page or tab, an interaction changes origin unexpectedly, or an open call returns blocked, blank, or otherwise unexpected results, use tab list / tab <tab-id-or-label> / snapshot -i to recover state before retrying different URLs or fallback strategies. Only use wait with an explicit argument like milliseconds, --load <state>, --url <matcher>, --fn <js>, or --text <matcher>.",
79
- "For feed, timeline, or inbox reading tasks, focus on the main timeline/list region and read the first item there rather than unrelated composer or sidebar content.",
80
- "For read-only browsing tasks, prefer extracting the answer from the current snapshot, structured ref labels, or eval --stdin on the current page before navigating away. Only click into media viewers, detail routes, or new pages when the current view does not contain the needed information.",
81
- "For downloads, prefer download <selector> <path> when an element click should save a file. Do not rely on click alone when you need the downloaded file on disk.",
82
- "When using eval --stdin, scope checks and actions to the target element or route whenever possible instead of relying on broad page-wide text heuristics.",
83
- "When using eval --stdin for extraction, return the value you want instead of relying on console.log as the primary result channel.",
84
- "Do not call --help or other exploratory inspection commands unless the user explicitly asks for them or debugging the browser integration is necessary.",
85
- ] as const;
86
- const TOOL_PROMPT_GUIDELINES_PREFIX = ["Use this tool whenever the task requires a real browser or live web content."] as const;
87
- const TOOL_PROMPT_GUIDELINES_SUFFIX = [
88
- "Prefer this tool over bash for opening sites, reading docs on the web, clicking, filling, screenshots, eval, and batch workflows.",
89
- "Do not fall back to osascript, AppleScript, or generic browser-driving bash commands when this tool can do the job.",
90
- "Pass exact agent-browser CLI arguments in args, excluding the binary name.",
91
- "Use stdin for commands like eval --stdin and batch instead of shell heredocs.",
92
- "Let the extension-managed session handle the common path unless you explicitly need a fresh launch for upstream flags like --profile, --session-name, or --cdp.",
93
- "Use sessionMode=fresh when switching from an existing implicit session to a new profile/debug launch without inventing a fixed explicit session name; later auto calls will follow that new session.",
94
- ] as const;
95
-
96
82
  function buildMissingBinaryMessage(): string {
97
83
  return [
98
84
  "agent-browser is required but was not found on PATH.",
99
85
  "This project does not bundle agent-browser.",
100
- "Install it using the upstream docs:",
86
+ "Run `pi-agent-browser-doctor` for package/PATH diagnostics, then install agent-browser using the upstream docs:",
101
87
  "- https://agent-browser.dev/",
102
88
  "- https://github.com/vercel-labs/agent-browser",
103
89
  ].join("\n");
@@ -108,6 +94,20 @@ function buildInvocationPreview(effectiveArgs: string[]): string {
108
94
  return preview.length > 120 ? `${preview.slice(0, 117)}...` : preview;
109
95
  }
110
96
 
97
+ function buildWrapperRecoveryHint(options: {
98
+ pinnedBatchUnwrapMode?: PinnedBatchUnwrapMode;
99
+ sessionTabCorrection?: OpenResultTabCorrection;
100
+ }): string | undefined {
101
+ const wrapperManagedContexts = [
102
+ options.sessionTabCorrection ? "session tab correction" : undefined,
103
+ options.pinnedBatchUnwrapMode ? "pinned batch routing" : undefined,
104
+ ].filter((item): item is string => item !== undefined);
105
+ if (wrapperManagedContexts.length === 0) {
106
+ return undefined;
107
+ }
108
+ return `Wrapper recovery hint: this call used ${wrapperManagedContexts.join(" and ")}. Inspect details.effectiveArgs and details.sessionTabCorrection; if the selected tab looks wrong, run tab list for the same session before retrying.`;
109
+ }
110
+
111
111
  const DIRECT_AGENT_BROWSER_EXECUTABLE_PATTERN = /^(?:[.~]|\.\.?|\/)?(?:[^\s;&|]+\/)?agent-browser$/;
112
112
  const HARMLESS_AGENT_BROWSER_INSPECTION_PATTERN = /^\s*(?:command\s+-v|which|type\s+-P)\s+agent-browser\s*$/;
113
113
 
@@ -323,14 +323,54 @@ function extractStringResultField(data: unknown, fieldName: "title" | "url"): st
323
323
  return text.length > 0 ? text : undefined;
324
324
  }
325
325
 
326
- const SESSION_TAB_PINNING_EXCLUDED_COMMANDS = new Set(["batch", "close", "goto", "navigate", "open", "session", "tab"]);
326
+ const SESSION_TAB_PINNING_EXCLUDED_COMMANDS = new Set(["close", "goto", "navigate", "open", "session", "tab"]);
327
327
  const SESSION_TAB_POST_COMMAND_CORRECTION_EXCLUDED_COMMANDS = new Set(["batch", "close", "session", "tab"]);
328
328
 
329
+ type PinnedBatchUnwrapMode = "single-command" | "user-batch";
330
+
331
+ type AgentBrowserToolResult = AgentToolResult<unknown> & { isError?: boolean };
332
+
333
+ type BatchCommandStep = [string, ...string[]];
334
+
335
+ interface PinnedBatchPlan {
336
+ includeNavigationSummary: boolean;
337
+ steps: BatchCommandStep[];
338
+ unwrapMode: PinnedBatchUnwrapMode;
339
+ }
340
+
329
341
  interface SessionTabTarget {
330
342
  title?: string;
331
343
  url: string;
332
344
  }
333
345
 
346
+ interface OrderedSessionTabTarget {
347
+ order: number;
348
+ target: SessionTabTarget;
349
+ }
350
+
351
+ interface AboutBlankSessionMismatch {
352
+ activeUrl: "about:blank";
353
+ recoveryApplied: boolean;
354
+ recoveryHint: string;
355
+ targetTitle?: string;
356
+ targetUrl: string;
357
+ }
358
+
359
+ function getLatestSessionTabTargetOrder(targets: Map<string, OrderedSessionTabTarget>): number {
360
+ let latestOrder = 0;
361
+ for (const target of targets.values()) {
362
+ latestOrder = Math.max(latestOrder, target.order);
363
+ }
364
+ return latestOrder;
365
+ }
366
+
367
+ function shouldApplySessionTabTargetUpdate(options: {
368
+ current?: OrderedSessionTabTarget;
369
+ updateOrder: number;
370
+ }): boolean {
371
+ return !options.current || options.updateOrder >= options.current.order;
372
+ }
373
+
334
374
  function normalizeComparableUrl(url: string | undefined): string | undefined {
335
375
  const normalizedUrl = url?.trim();
336
376
  if (!normalizedUrl) {
@@ -345,6 +385,26 @@ function normalizeComparableUrl(url: string | undefined): string | undefined {
345
385
  }
346
386
  }
347
387
 
388
+ function isAboutBlankUrl(url: string | undefined): boolean {
389
+ return normalizeComparableUrl(url) === "about:blank";
390
+ }
391
+
392
+ function isAboutBlankSessionTabTarget(target: SessionTabTarget | undefined): boolean {
393
+ return isAboutBlankUrl(target?.url);
394
+ }
395
+
396
+ function commandExplicitlyTargetsAboutBlank(commandTokens: string[]): boolean {
397
+ return commandTokens.some((token) => isAboutBlankUrl(token));
398
+ }
399
+
400
+ function buildAboutBlankRecoveryHint(): string {
401
+ 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.";
402
+ }
403
+
404
+ function buildAboutBlankWarning(mismatch: AboutBlankSessionMismatch): string {
405
+ 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."}`;
406
+ }
407
+
348
408
  function normalizeSessionTabTarget(target: { title?: string; url?: string } | undefined): SessionTabTarget | undefined {
349
409
  if (!target) {
350
410
  return undefined;
@@ -371,8 +431,50 @@ function extractSessionTabTargetFromData(data: unknown): SessionTabTarget | unde
371
431
  return undefined;
372
432
  }
373
433
 
374
- function restoreSessionTabTargetsFromBranch(branch: unknown[]): Map<string, SessionTabTarget> {
375
- const restoredTargets = new Map<string, SessionTabTarget>();
434
+ function extractBatchResultCommand(item: Record<string, unknown>): string[] {
435
+ return Array.isArray(item.command) ? item.command.filter((token): token is string => typeof token === "string") : [];
436
+ }
437
+
438
+ function extractSessionTabTargetFromBatchResults(data: unknown): SessionTabTarget | undefined {
439
+ if (!Array.isArray(data)) {
440
+ return undefined;
441
+ }
442
+
443
+ let currentTarget: SessionTabTarget | undefined;
444
+ let pendingTitle: string | undefined;
445
+ for (const item of data) {
446
+ if (!isRecord(item) || item.success === false) {
447
+ continue;
448
+ }
449
+ const [name, subcommand] = extractBatchResultCommand(item);
450
+ const result = item.result;
451
+
452
+ if (name === "get" && subcommand === "title") {
453
+ pendingTitle = extractStringResultField(result, "title");
454
+ continue;
455
+ }
456
+ if (name === "get" && subcommand === "url") {
457
+ const url = extractStringResultField(result, "url");
458
+ const target = normalizeSessionTabTarget({ title: pendingTitle, url });
459
+ if (target) {
460
+ currentTarget = target;
461
+ }
462
+ pendingTitle = undefined;
463
+ continue;
464
+ }
465
+
466
+ const resultTarget = extractSessionTabTargetFromData(result);
467
+ if (resultTarget) {
468
+ currentTarget = resultTarget;
469
+ }
470
+ pendingTitle = undefined;
471
+ }
472
+ return currentTarget;
473
+ }
474
+
475
+ function restoreSessionTabTargetsFromBranch(branch: unknown[]): Map<string, OrderedSessionTabTarget> {
476
+ const restoredTargets = new Map<string, OrderedSessionTabTarget>();
477
+ let restoredOrder = 0;
376
478
  for (const entry of branch) {
377
479
  if (!isRecord(entry) || entry.type !== "message") {
378
480
  continue;
@@ -391,6 +493,7 @@ function restoreSessionTabTargetsFromBranch(branch: unknown[]): Map<string, Sess
391
493
  }
392
494
  const command = typeof details.command === "string" ? details.command : undefined;
393
495
  if (command === "close" && message.isError !== true) {
496
+ restoredOrder += 1;
394
497
  restoredTargets.delete(sessionName);
395
498
  continue;
396
499
  }
@@ -401,21 +504,152 @@ function restoreSessionTabTargetsFromBranch(branch: unknown[]): Map<string, Sess
401
504
  })
402
505
  : undefined;
403
506
  if (sessionTabTarget) {
404
- restoredTargets.set(sessionName, sessionTabTarget);
507
+ restoredOrder += 1;
508
+ restoredTargets.set(sessionName, { order: restoredOrder, target: sessionTabTarget });
405
509
  }
406
510
  }
407
511
  return restoredTargets;
408
512
  }
409
513
 
410
- function shouldPinSessionTabForCommand(options: { command?: string; sessionName?: string; stdin?: string }): boolean {
514
+ function restoreArtifactManifestFromBranch(branch: unknown[]): SessionArtifactManifest | undefined {
515
+ let restoredManifest: SessionArtifactManifest | undefined;
516
+ for (const entry of branch) {
517
+ if (!isRecord(entry) || entry.type !== "message") continue;
518
+ const message = isRecord(entry.message) ? entry.message : undefined;
519
+ if (!message || message.toolName !== "agent_browser") continue;
520
+ const details = isRecord(message.details) ? message.details : undefined;
521
+ if (isSessionArtifactManifest(details?.artifactManifest)) {
522
+ restoredManifest = details.artifactManifest;
523
+ }
524
+ }
525
+ return restoredManifest;
526
+ }
527
+
528
+ function validateStdinCommandContract(options: { command?: string; commandTokens: string[]; stdin?: string }): string | undefined {
529
+ if (options.stdin === undefined) {
530
+ return undefined;
531
+ }
532
+ if (options.command === "batch") {
533
+ return undefined;
534
+ }
535
+ if (options.command === "eval" && options.commandTokens.includes("--stdin")) {
536
+ return undefined;
537
+ }
538
+ const commandLabel = options.command ? `\`${options.command}\`` : "the requested command";
539
+ return `agent_browser stdin is only supported for \`batch\` and \`eval --stdin\`; remove stdin from ${commandLabel} or use one of those command forms.`;
540
+ }
541
+
542
+ function supportsPinnedStdinCommand(options: { command?: string; commandTokens: string[]; stdin?: string }): boolean {
543
+ if (options.command === "batch") {
544
+ return options.stdin !== undefined;
545
+ }
546
+ if (options.stdin === undefined) {
547
+ return true;
548
+ }
549
+ if (options.command === "eval") {
550
+ return options.commandTokens.includes("--stdin");
551
+ }
552
+ return false;
553
+ }
554
+
555
+ function shouldPinSessionTabForCommand(options: {
556
+ command?: string;
557
+ commandTokens: string[];
558
+ sessionName?: string;
559
+ stdin?: string;
560
+ }): boolean {
411
561
  return (
412
562
  options.sessionName !== undefined &&
413
- options.stdin === undefined &&
414
563
  options.command !== undefined &&
415
- !SESSION_TAB_PINNING_EXCLUDED_COMMANDS.has(options.command)
564
+ !SESSION_TAB_PINNING_EXCLUDED_COMMANDS.has(options.command) &&
565
+ supportsPinnedStdinCommand(options)
416
566
  );
417
567
  }
418
568
 
569
+ function validateUserBatchStep(
570
+ step: unknown,
571
+ index: number,
572
+ ):
573
+ | { ok: true; step: BatchCommandStep }
574
+ | { ok: false; error: string } {
575
+ if (!Array.isArray(step)) {
576
+ return {
577
+ ok: false,
578
+ error: `agent_browser batch stdin step ${index} must be a non-empty array of string command tokens.`,
579
+ };
580
+ }
581
+ if (step.length === 0) {
582
+ return {
583
+ ok: false,
584
+ error: `agent_browser batch stdin step ${index} must not be empty.`,
585
+ };
586
+ }
587
+ const invalidTokenIndex = step.findIndex((token) => typeof token !== "string");
588
+ if (invalidTokenIndex !== -1) {
589
+ return {
590
+ ok: false,
591
+ error: `agent_browser batch stdin step ${index} token ${invalidTokenIndex} must be a string.`,
592
+ };
593
+ }
594
+ return { ok: true, step: step as BatchCommandStep };
595
+ }
596
+
597
+ function parseUserBatchStdin(stdin: string | undefined): { error?: string; steps?: BatchCommandStep[] } {
598
+ if (stdin === undefined) {
599
+ return { steps: [] };
600
+ }
601
+ try {
602
+ const parsed = JSON.parse(stdin) as unknown;
603
+ if (!Array.isArray(parsed)) {
604
+ return { error: "agent_browser batch stdin must be a JSON array of command steps." };
605
+ }
606
+ const steps: BatchCommandStep[] = [];
607
+ for (const [index, rawStep] of parsed.entries()) {
608
+ const validated = validateUserBatchStep(rawStep, index);
609
+ if (!validated.ok) {
610
+ return { error: validated.error };
611
+ }
612
+ steps.push(validated.step);
613
+ }
614
+ return { steps };
615
+ } catch (error) {
616
+ const message = error instanceof Error ? error.message : String(error);
617
+ return { error: `agent_browser batch stdin could not be parsed as JSON: ${message}` };
618
+ }
619
+ }
620
+
621
+ function buildPinnedBatchPlan(options: {
622
+ command?: string;
623
+ commandTokens: string[];
624
+ selectedTab: string;
625
+ stdin?: string;
626
+ }): PinnedBatchPlan | { error: string } | undefined {
627
+ if (options.command === "batch") {
628
+ const parsed = parseUserBatchStdin(options.stdin);
629
+ if (parsed.error) {
630
+ return { error: parsed.error };
631
+ }
632
+ const tabSelectionStep: BatchCommandStep = ["tab", options.selectedTab];
633
+ return {
634
+ includeNavigationSummary: false,
635
+ steps: [tabSelectionStep, ...(parsed.steps ?? [])],
636
+ unwrapMode: "user-batch",
637
+ };
638
+ }
639
+ if (options.commandTokens.length === 0) {
640
+ return undefined;
641
+ }
642
+ const includeNavigationSummary = options.command !== undefined && NAVIGATION_SUMMARY_COMMANDS.has(options.command);
643
+ const tabSelectionStep: BatchCommandStep = ["tab", options.selectedTab];
644
+ const commandStep = options.commandTokens as BatchCommandStep;
645
+ const navigationSummarySteps: BatchCommandStep[] = includeNavigationSummary ? [["get", "title"], ["get", "url"]] : [];
646
+ return {
647
+ includeNavigationSummary,
648
+ steps: [tabSelectionStep, commandStep, ...navigationSummarySteps],
649
+ unwrapMode: "single-command",
650
+ };
651
+ }
652
+
419
653
  function shouldCorrectSessionTabAfterCommand(options: { command?: string; sessionName?: string }): boolean {
420
654
  return (
421
655
  options.sessionName !== undefined &&
@@ -446,6 +680,7 @@ function deriveSessionTabTarget(options: {
446
680
  }
447
681
  return (
448
682
  normalizeSessionTabTarget(options.navigationSummary) ??
683
+ extractSessionTabTargetFromBatchResults(options.data) ??
449
684
  extractSessionTabTargetFromData(options.data) ??
450
685
  options.previousTarget
451
686
  );
@@ -454,6 +689,7 @@ function deriveSessionTabTarget(options: {
454
689
  function unwrapPinnedSessionBatchEnvelope(options: {
455
690
  envelope?: AgentBrowserEnvelope;
456
691
  includeNavigationSummary: boolean;
692
+ mode?: PinnedBatchUnwrapMode;
457
693
  }): { envelope?: AgentBrowserEnvelope; navigationSummary?: NavigationSummary; parseError?: string } {
458
694
  if (!options.envelope) {
459
695
  return {};
@@ -467,19 +703,29 @@ function unwrapPinnedSessionBatchEnvelope(options: {
467
703
  const steps = options.envelope.data.filter(isRecord) as AgentBrowserBatchResult[];
468
704
  const tabSelectionStep = steps[0];
469
705
  const commandStep = steps[1];
470
- if (!commandStep) {
706
+ if (tabSelectionStep?.success === false) {
471
707
  return {
472
708
  envelope: {
473
709
  success: false,
474
- error: "agent-browser did not return the corrected command result.",
710
+ error: tabSelectionStep.error ?? "agent-browser could not re-select the intended tab before running the command.",
475
711
  },
476
712
  };
477
713
  }
478
- if (tabSelectionStep?.success === false) {
714
+ if (options.mode === "user-batch") {
715
+ const userSteps = steps.slice(1);
716
+ return {
717
+ envelope: {
718
+ success: userSteps.every((step) => step.success !== false),
719
+ data: userSteps,
720
+ error: userSteps.find((step) => step.success === false)?.error,
721
+ },
722
+ };
723
+ }
724
+ if (!commandStep) {
479
725
  return {
480
726
  envelope: {
481
727
  success: false,
482
- error: tabSelectionStep.error ?? "agent-browser could not re-select the intended tab before running the command.",
728
+ error: "agent-browser did not return the corrected command result.",
483
729
  },
484
730
  };
485
731
  }
@@ -514,14 +760,14 @@ async function runSessionCommandData(options: {
514
760
  cwd,
515
761
  signal,
516
762
  });
517
- if (processResult.aborted || processResult.spawnError || processResult.exitCode !== 0) {
518
- return undefined;
519
- }
520
- const parsed = await parseAgentBrowserEnvelope({
521
- stdout: processResult.stdout,
522
- stdoutPath: processResult.stdoutSpillPath,
523
- });
524
763
  try {
764
+ if (processResult.aborted || processResult.spawnError || processResult.exitCode !== 0) {
765
+ return undefined;
766
+ }
767
+ const parsed = await parseAgentBrowserEnvelope({
768
+ stdout: processResult.stdout,
769
+ stdoutPath: processResult.stdoutSpillPath,
770
+ });
525
771
  if (parsed.parseError || parsed.envelope?.success === false) {
526
772
  return undefined;
527
773
  }
@@ -619,23 +865,6 @@ async function applyOpenResultTabCorrection(options: {
619
865
  return result === undefined ? undefined : correction;
620
866
  }
621
867
 
622
- function buildSharedBrowserPlaybookGuidelines(hasBraveApiKey: boolean): string[] {
623
- return [
624
- SHARED_BROWSER_PLAYBOOK_GUIDELINES[0],
625
- ...(hasBraveApiKey ? [BRAVE_SEARCH_PROMPT_GUIDELINE] : []),
626
- ...SHARED_BROWSER_PLAYBOOK_GUIDELINES.slice(1),
627
- ];
628
- }
629
-
630
- function buildToolPromptGuidelines(hasBraveApiKey: boolean): string[] {
631
- return [
632
- ...TOOL_PROMPT_GUIDELINES_PREFIX,
633
- ...QUICK_START_GUIDELINES,
634
- ...buildSharedBrowserPlaybookGuidelines(hasBraveApiKey),
635
- ...TOOL_PROMPT_GUIDELINES_SUFFIX,
636
- ];
637
- }
638
-
639
868
  function buildSessionDetailFields(sessionName: string | undefined, usedImplicitSession: boolean): Record<string, unknown> {
640
869
  return sessionName ? { sessionName, usedImplicitSession } : {};
641
870
  }
@@ -656,6 +885,70 @@ function getPersistentSessionArtifactStore(ctx: {
656
885
  return { sessionDir, sessionId };
657
886
  }
658
887
 
888
+ async function preserveParseFailureOutput(options: {
889
+ artifactManifest?: SessionArtifactManifest;
890
+ persistentArtifactStore?: PersistentSessionArtifactStore;
891
+ stdoutSpillPath?: string;
892
+ }): Promise<{
893
+ artifactManifest?: SessionArtifactManifest;
894
+ artifactRetentionSummary?: string;
895
+ fullOutputPath?: string;
896
+ fullOutputUnavailable?: string;
897
+ }> {
898
+ if (!options.stdoutSpillPath) {
899
+ return {};
900
+ }
901
+
902
+ try {
903
+ const rawOutput = await readFile(options.stdoutSpillPath);
904
+ const nowMs = Date.now();
905
+ let evictedArtifacts: PersistentSessionArtifactEviction[] = [];
906
+ let fullOutputPath: string;
907
+ let storageScope: "persistent-session" | "process-temp";
908
+ if (options.persistentArtifactStore) {
909
+ const result = await writePersistentSessionArtifactFile({
910
+ content: rawOutput,
911
+ prefix: "pi-agent-browser-parse-failure-output",
912
+ store: options.persistentArtifactStore,
913
+ suffix: ".txt",
914
+ });
915
+ fullOutputPath = result.path;
916
+ evictedArtifacts = result.evictedArtifacts;
917
+ storageScope = "persistent-session";
918
+ } else {
919
+ fullOutputPath = await writeSecureTempFile({
920
+ content: rawOutput,
921
+ prefix: "pi-agent-browser-parse-failure-output",
922
+ suffix: ".txt",
923
+ });
924
+ storageScope = "process-temp";
925
+ }
926
+ const artifactManifest = mergeSessionArtifactManifest({
927
+ base: options.artifactManifest,
928
+ entries: [
929
+ {
930
+ command: "agent-browser",
931
+ createdAtMs: nowMs,
932
+ kind: "spill",
933
+ path: fullOutputPath,
934
+ retentionState: storageScope === "persistent-session" ? "live" : "ephemeral",
935
+ storageScope,
936
+ },
937
+ ...buildEvictedSessionArtifactEntries(evictedArtifacts, nowMs),
938
+ ],
939
+ nowMs,
940
+ });
941
+ return {
942
+ artifactManifest,
943
+ artifactRetentionSummary: artifactManifest ? formatSessionArtifactRetentionSummary(artifactManifest) : undefined,
944
+ fullOutputPath,
945
+ };
946
+ } catch (error) {
947
+ const message = error instanceof Error ? error.message : String(error);
948
+ return { fullOutputUnavailable: message };
949
+ }
950
+ }
951
+
659
952
  function redactRecoveryHint(recoveryHint: {
660
953
  exampleArgs: string[];
661
954
  exampleParams: { args: string[]; sessionMode: "fresh" };
@@ -676,26 +969,53 @@ function redactRecoveryHint(recoveryHint: {
676
969
  };
677
970
  }
678
971
 
972
+ // Serializes managed-session read/modify/write work so overlapping tool calls cannot promote stale state or close an in-use session.
973
+ class AsyncExecutionQueue {
974
+ private tail: Promise<void> = Promise.resolve();
975
+
976
+ run<T>(work: () => Promise<T>): Promise<T> {
977
+ const previous = this.tail;
978
+ let release!: () => void;
979
+ this.tail = new Promise<void>((resolve) => {
980
+ release = resolve;
981
+ });
982
+
983
+ return (async () => {
984
+ await previous;
985
+ try {
986
+ return await work();
987
+ } finally {
988
+ release();
989
+ }
990
+ })();
991
+ }
992
+ }
993
+
679
994
  async function closeManagedSession(options: { cwd: string; sessionName: string; timeoutMs: number }): Promise<void> {
680
995
  const controller = new AbortController();
681
996
  const timer = setTimeout(() => controller.abort(), options.timeoutMs);
997
+ let stdoutSpillPath: string | undefined;
682
998
  try {
683
- await runAgentBrowserProcess({
999
+ const processResult = await runAgentBrowserProcess({
684
1000
  args: ["--session", options.sessionName, "close"],
685
1001
  cwd: options.cwd,
686
1002
  signal: controller.signal,
687
1003
  });
1004
+ stdoutSpillPath = processResult.stdoutSpillPath;
688
1005
  } catch {
689
1006
  // Best-effort cleanup only.
690
1007
  } finally {
691
1008
  clearTimeout(timer);
1009
+ if (stdoutSpillPath) {
1010
+ await rm(stdoutSpillPath, { force: true }).catch(() => undefined);
1011
+ }
692
1012
  }
693
1013
  }
694
1014
 
695
1015
  export default function agentBrowserExtension(pi: ExtensionAPI) {
696
1016
  const ephemeralSessionSeed = createEphemeralSessionSeed();
697
1017
  const hasBraveApiKey = hasUsableBraveApiKey();
698
- const toolPromptGuidelines = buildToolPromptGuidelines(hasBraveApiKey);
1018
+ const toolPromptGuidelines = buildToolPromptGuidelines({ includeBraveSearch: hasBraveApiKey });
699
1019
  const implicitSessionIdleTimeoutMs = getImplicitSessionIdleTimeoutMs();
700
1020
  const implicitSessionCloseTimeoutMs = getImplicitSessionCloseTimeoutMs();
701
1021
  let managedSessionActive = false;
@@ -703,7 +1023,10 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
703
1023
  let managedSessionName = managedSessionBaseName;
704
1024
  let managedSessionCwd = process.cwd();
705
1025
  let freshSessionOrdinal = 0;
706
- let sessionTabTargets = new Map<string, SessionTabTarget>();
1026
+ let sessionTabTargets = new Map<string, OrderedSessionTabTarget>();
1027
+ let sessionTabTargetUpdateOrder = 0;
1028
+ let artifactManifest: SessionArtifactManifest | undefined;
1029
+ const managedSessionExecutionQueue = new AsyncExecutionQueue();
707
1030
 
708
1031
  pi.on("session_start", async (_event, ctx) => {
709
1032
  managedSessionBaseName = createImplicitSessionName(ctx.sessionManager.getSessionId(), ctx.cwd, ephemeralSessionSeed);
@@ -713,11 +1036,15 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
713
1036
  managedSessionCwd = ctx.cwd;
714
1037
  freshSessionOrdinal = restoredState.freshSessionOrdinal;
715
1038
  sessionTabTargets = restoreSessionTabTargetsFromBranch(ctx.sessionManager.getBranch());
1039
+ sessionTabTargetUpdateOrder = getLatestSessionTabTargetOrder(sessionTabTargets);
1040
+ artifactManifest = restoreArtifactManifestFromBranch(ctx.sessionManager.getBranch());
716
1041
  });
717
1042
 
718
1043
  pi.on("session_shutdown", async () => {
719
1044
  managedSessionActive = false;
720
- sessionTabTargets = new Map<string, SessionTabTarget>();
1045
+ sessionTabTargets = new Map<string, OrderedSessionTabTarget>();
1046
+ sessionTabTargetUpdateOrder = 0;
1047
+ artifactManifest = undefined;
721
1048
  await cleanupSecureTempArtifacts();
722
1049
  });
723
1050
 
@@ -765,312 +1092,488 @@ export default function agentBrowserExtension(pi: ExtensionAPI) {
765
1092
  };
766
1093
  }
767
1094
 
768
- const sessionMode = params.sessionMode ?? DEFAULT_SESSION_MODE;
769
- const freshSessionName = createFreshSessionName(managedSessionBaseName, ephemeralSessionSeed, freshSessionOrdinal + 1);
770
- const executionPlan = buildExecutionPlan(params.args, {
771
- freshSessionName,
772
- managedSessionActive,
773
- managedSessionName,
774
- sessionMode,
775
- });
776
- const redactedEffectiveArgs = redactInvocationArgs(executionPlan.effectiveArgs);
777
- const redactedRecoveryHint = redactRecoveryHint(executionPlan.recoveryHint);
778
- const compatibilityWorkaround: CompatibilityWorkaround | undefined = executionPlan.compatibilityWorkaround;
779
- if (executionPlan.managedSessionName === freshSessionName) {
780
- freshSessionOrdinal += 1;
781
- }
1095
+ const tabTargetUpdateOrder = ++sessionTabTargetUpdateOrder;
1096
+ const runTool = async (): Promise<AgentBrowserToolResult> => {
1097
+ const sessionMode = params.sessionMode ?? DEFAULT_SESSION_MODE;
1098
+ const freshSessionName = createFreshSessionName(managedSessionBaseName, ephemeralSessionSeed, freshSessionOrdinal + 1);
1099
+ const executionPlan = buildExecutionPlan(params.args, {
1100
+ freshSessionName,
1101
+ managedSessionActive,
1102
+ managedSessionName,
1103
+ sessionMode,
1104
+ });
1105
+ const redactedEffectiveArgs = redactInvocationArgs(executionPlan.effectiveArgs);
1106
+ const redactedRecoveryHint = redactRecoveryHint(executionPlan.recoveryHint);
1107
+ const compatibilityWorkaround: CompatibilityWorkaround | undefined = executionPlan.compatibilityWorkaround;
1108
+ if (executionPlan.managedSessionName === freshSessionName) {
1109
+ freshSessionOrdinal += 1;
1110
+ }
782
1111
 
783
- if (executionPlan.validationError) {
784
- return {
785
- content: [{ type: "text", text: executionPlan.validationError }],
786
- details: {
787
- args: redactedArgs,
788
- invalidValueFlag: executionPlan.invalidValueFlag,
789
- sessionMode,
790
- sessionRecoveryHint: redactedRecoveryHint,
791
- startupScopedFlags: executionPlan.startupScopedFlags,
792
- validationError: executionPlan.validationError,
793
- },
794
- isError: true,
795
- };
796
- }
1112
+ if (executionPlan.validationError) {
1113
+ return {
1114
+ content: [{ type: "text", text: executionPlan.validationError }],
1115
+ details: {
1116
+ args: redactedArgs,
1117
+ invalidValueFlag: executionPlan.invalidValueFlag,
1118
+ sessionMode,
1119
+ sessionRecoveryHint: redactedRecoveryHint,
1120
+ startupScopedFlags: executionPlan.startupScopedFlags,
1121
+ validationError: executionPlan.validationError,
1122
+ },
1123
+ isError: true,
1124
+ };
1125
+ }
797
1126
 
798
- const priorSessionTabTarget = executionPlan.sessionName ? sessionTabTargets.get(executionPlan.sessionName) : undefined;
799
- const includePinnedNavigationSummary =
800
- executionPlan.commandInfo.command !== undefined && NAVIGATION_SUMMARY_COMMANDS.has(executionPlan.commandInfo.command);
801
- let sessionTabCorrection: OpenResultTabCorrection | undefined;
802
- let processArgs = executionPlan.effectiveArgs;
803
- let processStdin = params.stdin;
804
- if (
805
- priorSessionTabTarget &&
806
- shouldPinSessionTabForCommand({
1127
+ const commandTokens = extractCommandTokens(params.args);
1128
+ const stdinValidationError = validateStdinCommandContract({
807
1129
  command: executionPlan.commandInfo.command,
808
- sessionName: executionPlan.sessionName,
1130
+ commandTokens,
809
1131
  stdin: params.stdin,
810
- })
811
- ) {
812
- const plannedSessionTabSelection = await collectSessionTabSelection({
813
- cwd: ctx.cwd,
814
- sessionName: executionPlan.sessionName,
815
- signal,
816
- target: priorSessionTabTarget,
817
1132
  });
818
- const commandTokens = extractCommandTokens(params.args);
819
- if (plannedSessionTabSelection && commandTokens.length > 0 && executionPlan.sessionName) {
820
- sessionTabCorrection = plannedSessionTabSelection;
821
- processArgs = ["--json", "--session", executionPlan.sessionName, "batch"];
822
- processStdin = JSON.stringify([
823
- ["tab", plannedSessionTabSelection.selectedTab],
824
- commandTokens,
825
- ...(includePinnedNavigationSummary ? [["get", "title"], ["get", "url"]] : []),
826
- ]);
1133
+ if (stdinValidationError) {
1134
+ return {
1135
+ content: [{ type: "text", text: stdinValidationError }],
1136
+ details: {
1137
+ args: redactedArgs,
1138
+ command: executionPlan.commandInfo.command,
1139
+ compatibilityWorkaround,
1140
+ effectiveArgs: redactedEffectiveArgs,
1141
+ sessionMode,
1142
+ validationError: stdinValidationError,
1143
+ ...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
1144
+ },
1145
+ isError: true,
1146
+ };
827
1147
  }
828
- }
829
- const redactedProcessArgs = redactInvocationArgs(processArgs);
830
1148
 
831
- onUpdate?.({
832
- content: [{ type: "text", text: `Running agent-browser ${buildInvocationPreview(redactedProcessArgs)}` }],
833
- details: {
834
- compatibilityWorkaround,
835
- effectiveArgs: redactedProcessArgs,
836
- sessionMode,
837
- sessionTabCorrection,
838
- ...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
839
- },
840
- });
841
-
842
- const processResult = await runAgentBrowserProcess({
843
- args: processArgs,
844
- cwd: ctx.cwd,
845
- env: executionPlan.managedSessionName ? { AGENT_BROWSER_IDLE_TIMEOUT_MS: implicitSessionIdleTimeoutMs } : undefined,
846
- signal,
847
- stdin: processStdin,
848
- });
1149
+ const priorSessionTabTargetState = executionPlan.sessionName ? sessionTabTargets.get(executionPlan.sessionName) : undefined;
1150
+ const priorSessionTabTarget = priorSessionTabTargetState?.target;
1151
+ let pinnedBatchUnwrapMode: PinnedBatchUnwrapMode | undefined;
1152
+ let includePinnedNavigationSummary = false;
1153
+ let sessionTabCorrection: OpenResultTabCorrection | undefined;
1154
+ let processArgs = executionPlan.effectiveArgs;
1155
+ let processStdin = params.stdin;
1156
+ if (
1157
+ priorSessionTabTarget &&
1158
+ shouldPinSessionTabForCommand({
1159
+ command: executionPlan.commandInfo.command,
1160
+ commandTokens,
1161
+ sessionName: executionPlan.sessionName,
1162
+ stdin: params.stdin,
1163
+ })
1164
+ ) {
1165
+ const plannedSessionTabSelection = await collectSessionTabSelection({
1166
+ cwd: ctx.cwd,
1167
+ sessionName: executionPlan.sessionName,
1168
+ signal,
1169
+ target: priorSessionTabTarget,
1170
+ });
1171
+ if (plannedSessionTabSelection && executionPlan.sessionName) {
1172
+ if (executionPlan.commandInfo.command === "eval" && params.stdin !== undefined) {
1173
+ const appliedSessionTabSelection = await applyOpenResultTabCorrection({
1174
+ correction: plannedSessionTabSelection,
1175
+ cwd: ctx.cwd,
1176
+ sessionName: executionPlan.sessionName,
1177
+ signal,
1178
+ });
1179
+ if (!appliedSessionTabSelection) {
1180
+ const error = "agent-browser could not re-select the intended tab before running the command.";
1181
+ return {
1182
+ content: [{ type: "text", text: error }],
1183
+ details: {
1184
+ args: redactedArgs,
1185
+ command: executionPlan.commandInfo.command,
1186
+ compatibilityWorkaround,
1187
+ effectiveArgs: redactedEffectiveArgs,
1188
+ sessionMode,
1189
+ sessionTabCorrection: plannedSessionTabSelection,
1190
+ validationError: error,
1191
+ ...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
1192
+ },
1193
+ isError: true,
1194
+ };
1195
+ }
1196
+ sessionTabCorrection = appliedSessionTabSelection;
1197
+ } else {
1198
+ const pinnedBatchPlan = buildPinnedBatchPlan({
1199
+ command: executionPlan.commandInfo.command,
1200
+ commandTokens,
1201
+ selectedTab: plannedSessionTabSelection.selectedTab,
1202
+ stdin: params.stdin,
1203
+ });
1204
+ if (pinnedBatchPlan && "error" in pinnedBatchPlan) {
1205
+ return {
1206
+ content: [{ type: "text", text: pinnedBatchPlan.error }],
1207
+ details: {
1208
+ args: redactedArgs,
1209
+ command: executionPlan.commandInfo.command,
1210
+ compatibilityWorkaround,
1211
+ effectiveArgs: redactedEffectiveArgs,
1212
+ sessionMode,
1213
+ sessionTabCorrection: plannedSessionTabSelection,
1214
+ validationError: pinnedBatchPlan.error,
1215
+ ...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
1216
+ },
1217
+ isError: true,
1218
+ };
1219
+ }
1220
+ if (pinnedBatchPlan) {
1221
+ sessionTabCorrection = plannedSessionTabSelection;
1222
+ processArgs = ["--json", "--session", executionPlan.sessionName, "batch"];
1223
+ processStdin = JSON.stringify(pinnedBatchPlan.steps);
1224
+ includePinnedNavigationSummary = pinnedBatchPlan.includeNavigationSummary;
1225
+ pinnedBatchUnwrapMode = pinnedBatchPlan.unwrapMode;
1226
+ }
1227
+ }
1228
+ }
1229
+ }
1230
+ const redactedProcessArgs = redactInvocationArgs(processArgs);
849
1231
 
850
- if (processResult.spawnError?.message.includes("ENOENT")) {
851
- const errorText = buildMissingBinaryMessage();
852
- return {
853
- content: [{ type: "text", text: errorText }],
1232
+ onUpdate?.({
1233
+ content: [{ type: "text", text: `Running agent-browser ${buildInvocationPreview(redactedProcessArgs)}` }],
854
1234
  details: {
855
- args: redactedArgs,
856
1235
  compatibilityWorkaround,
857
1236
  effectiveArgs: redactedProcessArgs,
858
1237
  sessionMode,
859
1238
  sessionTabCorrection,
860
- spawnError: processResult.spawnError.message,
1239
+ ...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
861
1240
  },
862
- isError: true,
863
- };
864
- }
1241
+ });
865
1242
 
866
- try {
867
- const parsed = await parseAgentBrowserEnvelope({
868
- stdout: processResult.stdout,
869
- stdoutPath: processResult.stdoutSpillPath,
1243
+ const processResult = await runAgentBrowserProcess({
1244
+ args: processArgs,
1245
+ cwd: ctx.cwd,
1246
+ env: executionPlan.managedSessionName ? { AGENT_BROWSER_IDLE_TIMEOUT_MS: implicitSessionIdleTimeoutMs } : undefined,
1247
+ signal,
1248
+ stdin: processStdin,
870
1249
  });
871
- let parseError = parsed.parseError;
872
- let presentationEnvelope = parsed.envelope;
873
- let navigationSummary: NavigationSummary | undefined;
874
- if (sessionTabCorrection) {
875
- const pinnedBatchResult = unwrapPinnedSessionBatchEnvelope({
876
- envelope: parsed.envelope,
877
- includeNavigationSummary: includePinnedNavigationSummary,
878
- });
879
- parseError = pinnedBatchResult.parseError ?? parseError;
880
- presentationEnvelope = pinnedBatchResult.envelope ?? presentationEnvelope;
881
- navigationSummary = pinnedBatchResult.navigationSummary;
882
- }
883
- const processSucceeded = !processResult.aborted && !processResult.spawnError && processResult.exitCode === 0;
884
- const plainTextInspection = executionPlan.plainTextInspection && processSucceeded;
885
- const parseSucceeded = plainTextInspection || parseError === undefined;
886
- const envelopeSuccess = plainTextInspection ? true : presentationEnvelope?.success !== false;
887
- const succeeded = processSucceeded && parseSucceeded && envelopeSuccess;
888
- const inspectionText = plainTextInspection ? processResult.stdout.trim() : undefined;
889
-
890
- if (succeeded && !navigationSummary && shouldCaptureNavigationSummary(executionPlan.commandInfo.command, presentationEnvelope?.data)) {
891
- navigationSummary = await collectNavigationSummary({
892
- cwd: ctx.cwd,
893
- sessionName: executionPlan.sessionName,
894
- signal,
895
- });
896
- }
897
- if (navigationSummary && presentationEnvelope) {
898
- presentationEnvelope = {
899
- ...presentationEnvelope,
900
- data: mergeNavigationSummaryIntoData(presentationEnvelope.data, navigationSummary),
1250
+
1251
+ if (processResult.spawnError?.message.includes("ENOENT")) {
1252
+ const errorText = buildMissingBinaryMessage();
1253
+ return {
1254
+ content: [{ type: "text", text: errorText }],
1255
+ details: {
1256
+ args: redactedArgs,
1257
+ compatibilityWorkaround,
1258
+ effectiveArgs: redactedProcessArgs,
1259
+ sessionMode,
1260
+ sessionTabCorrection,
1261
+ spawnError: processResult.spawnError.message,
1262
+ },
1263
+ isError: true,
901
1264
  };
902
1265
  }
903
1266
 
904
- let openResultTabCorrection: OpenResultTabCorrection | undefined;
905
- if (
906
- succeeded &&
907
- executionPlan.sessionName &&
908
- params.args.some((token) => token === "--profile" || token.startsWith("--profile=")) &&
909
- (executionPlan.commandInfo.command === "goto" ||
910
- executionPlan.commandInfo.command === "navigate" ||
911
- executionPlan.commandInfo.command === "open")
912
- ) {
913
- const targetTitle = extractStringResultField(presentationEnvelope?.data, "title");
914
- const targetUrl = extractStringResultField(presentationEnvelope?.data, "url");
915
- const plannedTabCorrection = await collectOpenResultTabCorrection({
916
- cwd: ctx.cwd,
917
- sessionName: executionPlan.sessionName,
918
- signal,
919
- targetTitle,
920
- targetUrl,
1267
+ try {
1268
+ const persistentArtifactStore = getPersistentSessionArtifactStore(ctx);
1269
+ const parsed = await parseAgentBrowserEnvelope({
1270
+ stdout: processResult.stdout,
1271
+ stdoutPath: processResult.stdoutSpillPath,
921
1272
  });
922
- if (plannedTabCorrection) {
923
- openResultTabCorrection = await applyOpenResultTabCorrection({
924
- correction: plannedTabCorrection,
1273
+ let parseError = parsed.parseError;
1274
+ let presentationEnvelope = parsed.envelope;
1275
+ let navigationSummary: NavigationSummary | undefined;
1276
+ if (pinnedBatchUnwrapMode) {
1277
+ const pinnedBatchResult = unwrapPinnedSessionBatchEnvelope({
1278
+ envelope: parsed.envelope,
1279
+ includeNavigationSummary: includePinnedNavigationSummary,
1280
+ mode: pinnedBatchUnwrapMode,
1281
+ });
1282
+ parseError = pinnedBatchResult.parseError ?? parseError;
1283
+ presentationEnvelope = pinnedBatchResult.envelope ?? presentationEnvelope;
1284
+ navigationSummary = pinnedBatchResult.navigationSummary;
1285
+ }
1286
+ const parseFailureOutput = parseError
1287
+ ? await preserveParseFailureOutput({
1288
+ artifactManifest,
1289
+ persistentArtifactStore,
1290
+ stdoutSpillPath: processResult.stdoutSpillPath,
1291
+ })
1292
+ : {};
1293
+ const processSucceeded = !processResult.aborted && !processResult.spawnError && processResult.exitCode === 0;
1294
+ const plainTextInspection = executionPlan.plainTextInspection && processSucceeded;
1295
+ const parseSucceeded = plainTextInspection || parseError === undefined;
1296
+ const envelopeSuccess = plainTextInspection ? true : presentationEnvelope?.success !== false;
1297
+ const succeeded = processSucceeded && parseSucceeded && envelopeSuccess;
1298
+ const inspectionText = plainTextInspection ? processResult.stdout.trim() : undefined;
1299
+
1300
+ if (succeeded && !navigationSummary && shouldCaptureNavigationSummary(executionPlan.commandInfo.command, presentationEnvelope?.data)) {
1301
+ navigationSummary = await collectNavigationSummary({
925
1302
  cwd: ctx.cwd,
926
1303
  sessionName: executionPlan.sessionName,
927
1304
  signal,
928
1305
  });
929
1306
  }
930
- }
1307
+ if (navigationSummary && presentationEnvelope) {
1308
+ presentationEnvelope = {
1309
+ ...presentationEnvelope,
1310
+ data: mergeNavigationSummaryIntoData(presentationEnvelope.data, navigationSummary),
1311
+ };
1312
+ }
931
1313
 
932
- const observedSessionTabTarget =
933
- normalizeSessionTabTarget(navigationSummary) ?? extractSessionTabTargetFromData(presentationEnvelope?.data);
934
- const currentSessionTabTarget = deriveSessionTabTarget({
935
- command: executionPlan.commandInfo.command,
936
- data: presentationEnvelope?.data,
937
- navigationSummary,
938
- previousTarget: priorSessionTabTarget,
939
- });
940
- if (
941
- succeeded &&
942
- priorSessionTabTarget &&
943
- !sessionTabCorrection &&
944
- observedSessionTabTarget &&
945
- shouldCorrectSessionTabAfterCommand({
1314
+ let openResultTabCorrection: OpenResultTabCorrection | undefined;
1315
+ if (
1316
+ succeeded &&
1317
+ executionPlan.sessionName &&
1318
+ hasLaunchScopedTabCorrectionFlag(params.args) &&
1319
+ (executionPlan.commandInfo.command === "goto" ||
1320
+ executionPlan.commandInfo.command === "navigate" ||
1321
+ executionPlan.commandInfo.command === "open")
1322
+ ) {
1323
+ const targetTitle = extractStringResultField(presentationEnvelope?.data, "title");
1324
+ const targetUrl = extractStringResultField(presentationEnvelope?.data, "url");
1325
+ const plannedTabCorrection = await collectOpenResultTabCorrection({
1326
+ cwd: ctx.cwd,
1327
+ sessionName: executionPlan.sessionName,
1328
+ signal,
1329
+ targetTitle,
1330
+ targetUrl,
1331
+ });
1332
+ if (plannedTabCorrection) {
1333
+ openResultTabCorrection = await applyOpenResultTabCorrection({
1334
+ correction: plannedTabCorrection,
1335
+ cwd: ctx.cwd,
1336
+ sessionName: executionPlan.sessionName,
1337
+ signal,
1338
+ });
1339
+ }
1340
+ }
1341
+
1342
+ const observedSessionTabTarget =
1343
+ normalizeSessionTabTarget(navigationSummary) ??
1344
+ extractSessionTabTargetFromBatchResults(presentationEnvelope?.data) ??
1345
+ extractSessionTabTargetFromData(presentationEnvelope?.data);
1346
+ let currentSessionTabTarget = deriveSessionTabTarget({
946
1347
  command: executionPlan.commandInfo.command,
947
- sessionName: executionPlan.sessionName,
948
- })
949
- ) {
950
- const postCommandTabCorrection = await collectSessionTabSelection({
951
- cwd: ctx.cwd,
952
- sessionName: executionPlan.sessionName,
953
- signal,
954
- target: observedSessionTabTarget,
1348
+ data: presentationEnvelope?.data,
1349
+ navigationSummary,
1350
+ previousTarget: priorSessionTabTarget,
955
1351
  });
956
- if (postCommandTabCorrection) {
957
- const appliedPostCommandCorrection = await applyOpenResultTabCorrection({
958
- correction: postCommandTabCorrection,
1352
+ let aboutBlankSessionMismatch: AboutBlankSessionMismatch | undefined;
1353
+ const shouldTreatAboutBlankAsMismatch =
1354
+ succeeded &&
1355
+ priorSessionTabTarget !== undefined &&
1356
+ !isAboutBlankSessionTabTarget(priorSessionTabTarget) &&
1357
+ isAboutBlankSessionTabTarget(observedSessionTabTarget ?? currentSessionTabTarget) &&
1358
+ !commandExplicitlyTargetsAboutBlank(commandTokens);
1359
+ if (shouldTreatAboutBlankAsMismatch && priorSessionTabTarget) {
1360
+ const aboutBlankRecovery = await collectSessionTabSelection({
959
1361
  cwd: ctx.cwd,
960
1362
  sessionName: executionPlan.sessionName,
961
1363
  signal,
1364
+ target: priorSessionTabTarget,
962
1365
  });
963
- if (appliedPostCommandCorrection && !sessionTabCorrection) {
964
- sessionTabCorrection = appliedPostCommandCorrection;
1366
+ const appliedAboutBlankRecovery = aboutBlankRecovery
1367
+ ? await applyOpenResultTabCorrection({
1368
+ correction: aboutBlankRecovery,
1369
+ cwd: ctx.cwd,
1370
+ sessionName: executionPlan.sessionName,
1371
+ signal,
1372
+ })
1373
+ : undefined;
1374
+ if (appliedAboutBlankRecovery) {
1375
+ sessionTabCorrection = appliedAboutBlankRecovery;
965
1376
  }
1377
+ aboutBlankSessionMismatch = {
1378
+ activeUrl: "about:blank",
1379
+ recoveryApplied: appliedAboutBlankRecovery !== undefined,
1380
+ recoveryHint: buildAboutBlankRecoveryHint(),
1381
+ targetTitle: priorSessionTabTarget.title,
1382
+ targetUrl: priorSessionTabTarget.url,
1383
+ };
1384
+ currentSessionTabTarget = priorSessionTabTarget;
966
1385
  }
967
- }
968
- if (executionPlan.sessionName) {
969
- if (executionPlan.commandInfo.command === "close" && succeeded) {
970
- sessionTabTargets.delete(executionPlan.sessionName);
971
- } else if (currentSessionTabTarget) {
972
- sessionTabTargets.set(executionPlan.sessionName, currentSessionTabTarget);
1386
+ if (
1387
+ succeeded &&
1388
+ priorSessionTabTarget &&
1389
+ !sessionTabCorrection &&
1390
+ !aboutBlankSessionMismatch &&
1391
+ !commandExplicitlyTargetsAboutBlank(commandTokens) &&
1392
+ observedSessionTabTarget &&
1393
+ shouldCorrectSessionTabAfterCommand({
1394
+ command: executionPlan.commandInfo.command,
1395
+ sessionName: executionPlan.sessionName,
1396
+ })
1397
+ ) {
1398
+ const postCommandTabCorrection = await collectSessionTabSelection({
1399
+ cwd: ctx.cwd,
1400
+ sessionName: executionPlan.sessionName,
1401
+ signal,
1402
+ target: observedSessionTabTarget,
1403
+ });
1404
+ if (postCommandTabCorrection) {
1405
+ const appliedPostCommandCorrection = await applyOpenResultTabCorrection({
1406
+ correction: postCommandTabCorrection,
1407
+ cwd: ctx.cwd,
1408
+ sessionName: executionPlan.sessionName,
1409
+ signal,
1410
+ });
1411
+ if (appliedPostCommandCorrection && !sessionTabCorrection) {
1412
+ sessionTabCorrection = appliedPostCommandCorrection;
1413
+ }
1414
+ }
1415
+ }
1416
+ if (executionPlan.sessionName) {
1417
+ const activeSessionTabTargetState = sessionTabTargets.get(executionPlan.sessionName);
1418
+ if (shouldApplySessionTabTargetUpdate({ current: activeSessionTabTargetState, updateOrder: tabTargetUpdateOrder })) {
1419
+ if (executionPlan.commandInfo.command === "close" && succeeded) {
1420
+ sessionTabTargets.delete(executionPlan.sessionName);
1421
+ } else if (currentSessionTabTarget) {
1422
+ sessionTabTargets.set(executionPlan.sessionName, { order: tabTargetUpdateOrder, target: currentSessionTabTarget });
1423
+ }
1424
+ }
973
1425
  }
974
- }
975
1426
 
976
- const priorManagedSessionCwd = managedSessionCwd;
977
- const managedSessionState = resolveManagedSessionState({
978
- command: executionPlan.commandInfo.command,
979
- managedSessionName: executionPlan.managedSessionName,
980
- priorActive: managedSessionActive,
981
- priorSessionName: managedSessionName,
982
- succeeded,
983
- });
984
- const replacedManagedSessionName = managedSessionState.replacedSessionName;
985
- managedSessionActive = managedSessionState.active;
986
- managedSessionName = managedSessionState.sessionName;
987
- if (executionPlan.managedSessionName && succeeded) {
988
- managedSessionCwd = ctx.cwd;
989
- }
990
- if (replacedManagedSessionName) {
991
- sessionTabTargets.delete(replacedManagedSessionName);
992
- await closeManagedSession({
993
- cwd: priorManagedSessionCwd,
994
- sessionName: replacedManagedSessionName,
995
- timeoutMs: implicitSessionCloseTimeoutMs,
1427
+ const priorManagedSessionCwd = managedSessionCwd;
1428
+ const managedSessionState = resolveManagedSessionState({
1429
+ command: executionPlan.commandInfo.command,
1430
+ managedSessionName: executionPlan.managedSessionName,
1431
+ priorActive: managedSessionActive,
1432
+ priorSessionName: managedSessionName,
1433
+ succeeded,
996
1434
  });
997
- }
998
-
999
- const errorText = getAgentBrowserErrorText({
1000
- aborted: processResult.aborted,
1001
- envelope: presentationEnvelope,
1002
- exitCode: processResult.exitCode,
1003
- parseError,
1004
- plainTextInspection,
1005
- spawnError: processResult.spawnError,
1006
- stderr: processResult.stderr,
1007
- });
1008
-
1009
- const presentation = plainTextInspection
1010
- ? {
1011
- batchFailure: undefined,
1012
- batchSteps: undefined,
1013
- content: [{ type: "text" as const, text: inspectionText ?? "" }],
1014
- data: undefined,
1015
- fullOutputPath: undefined,
1016
- fullOutputPaths: undefined,
1017
- imagePath: undefined,
1018
- imagePaths: undefined,
1019
- summary: `${redactedArgs.join(" ")} completed`,
1020
- }
1021
- : await buildToolPresentation({
1022
- commandInfo: executionPlan.commandInfo,
1023
- cwd: ctx.cwd,
1024
- envelope: presentationEnvelope,
1025
- errorText,
1026
- persistentArtifactStore: getPersistentSessionArtifactStore(ctx),
1027
- });
1028
- const redactedContent = presentation.content.map((item) =>
1029
- item.type === "text" ? { ...item, text: redactSensitiveText(item.text) } : item,
1030
- );
1435
+ const replacedManagedSessionName = managedSessionState.replacedSessionName;
1436
+ managedSessionActive = managedSessionState.active;
1437
+ managedSessionName = managedSessionState.sessionName;
1438
+ if (executionPlan.managedSessionName && succeeded) {
1439
+ managedSessionCwd = ctx.cwd;
1440
+ }
1441
+ if (replacedManagedSessionName) {
1442
+ sessionTabTargets.delete(replacedManagedSessionName);
1443
+ await closeManagedSession({
1444
+ cwd: priorManagedSessionCwd,
1445
+ sessionName: replacedManagedSessionName,
1446
+ timeoutMs: implicitSessionCloseTimeoutMs,
1447
+ });
1448
+ }
1031
1449
 
1032
- return {
1033
- content: redactedContent,
1034
- details: {
1035
- args: redactedArgs,
1036
- batchFailure: redactSensitiveValue(presentation.batchFailure),
1037
- batchSteps: redactSensitiveValue(presentation.batchSteps),
1450
+ const errorText = getAgentBrowserErrorText({
1451
+ aborted: processResult.aborted,
1038
1452
  command: executionPlan.commandInfo.command,
1039
- compatibilityWorkaround,
1040
- subcommand: executionPlan.commandInfo.subcommand,
1041
- data: redactSensitiveValue(presentation.data),
1042
- error: plainTextInspection ? undefined : redactSensitiveValue(presentationEnvelope?.error),
1043
- inspection: plainTextInspection || undefined,
1044
- navigationSummary: redactSensitiveValue(navigationSummary),
1045
- openResultTabCorrection: redactSensitiveValue(openResultTabCorrection),
1046
1453
  effectiveArgs: redactedProcessArgs,
1454
+ envelope: presentationEnvelope,
1047
1455
  exitCode: processResult.exitCode,
1048
- fullOutputPath: presentation.fullOutputPath,
1049
- fullOutputPaths: presentation.fullOutputPaths,
1050
- imagePath: presentation.imagePath,
1051
- imagePaths: presentation.imagePaths,
1052
- parseError: plainTextInspection ? undefined : parseError,
1053
- sessionMode,
1054
- sessionTabCorrection: redactSensitiveValue(sessionTabCorrection),
1055
- sessionTabTarget: redactSensitiveValue(currentSessionTabTarget),
1056
- ...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
1057
- sessionRecoveryHint: redactedRecoveryHint,
1058
- startupScopedFlags: executionPlan.startupScopedFlags,
1059
- stderr: processResult.stderr ? redactSensitiveText(processResult.stderr) : undefined,
1060
- stdout: plainTextInspection
1061
- ? redactSensitiveText(inspectionText ?? "")
1062
- : parseSucceeded
1063
- ? undefined
1064
- : redactSensitiveText(processResult.stdout),
1065
- summary: redactSensitiveText(presentation.summary),
1066
- },
1067
- isError: !succeeded,
1068
- };
1069
- } finally {
1070
- if (processResult.stdoutSpillPath) {
1071
- await rm(processResult.stdoutSpillPath, { force: true }).catch(() => undefined);
1456
+ parseError,
1457
+ plainTextInspection,
1458
+ spawnError: processResult.spawnError,
1459
+ stderr: processResult.stderr,
1460
+ wrapperRecoveryHint: buildWrapperRecoveryHint({ pinnedBatchUnwrapMode, sessionTabCorrection }),
1461
+ });
1462
+
1463
+ const presentation = plainTextInspection
1464
+ ? {
1465
+ artifacts: undefined,
1466
+ batchFailure: undefined,
1467
+ batchSteps: undefined,
1468
+ content: [{ type: "text" as const, text: inspectionText ?? "" }],
1469
+ data: undefined,
1470
+ fullOutputPath: undefined,
1471
+ fullOutputPaths: undefined,
1472
+ imagePath: undefined,
1473
+ imagePaths: undefined,
1474
+ savedFile: undefined,
1475
+ savedFilePath: undefined,
1476
+ summary: `${redactedArgs.join(" ")} completed`,
1477
+ }
1478
+ : await buildToolPresentation({
1479
+ artifactManifest,
1480
+ commandInfo: executionPlan.commandInfo,
1481
+ cwd: ctx.cwd,
1482
+ envelope: presentationEnvelope,
1483
+ errorText,
1484
+ persistentArtifactStore,
1485
+ });
1486
+ if (parseFailureOutput.artifactManifest) {
1487
+ presentation.artifactManifest = parseFailureOutput.artifactManifest;
1488
+ presentation.artifactRetentionSummary = parseFailureOutput.artifactRetentionSummary;
1489
+ }
1490
+ if (parseFailureOutput.fullOutputPath || parseFailureOutput.fullOutputUnavailable) {
1491
+ const existingText = presentation.content[0]?.type === "text" ? presentation.content[0].text : "";
1492
+ const noticeLines = [
1493
+ parseFailureOutput.fullOutputPath
1494
+ ? `Full output path: ${parseFailureOutput.fullOutputPath}`
1495
+ : `Full raw output unavailable: ${parseFailureOutput.fullOutputUnavailable}`,
1496
+ parseFailureOutput.artifactRetentionSummary,
1497
+ ].filter((item): item is string => item !== undefined);
1498
+ const notice = noticeLines.join("\n");
1499
+ presentation.content[0] = {
1500
+ type: "text",
1501
+ text: existingText.length > 0 ? `${existingText}\n\n${notice}` : notice,
1502
+ };
1503
+ }
1504
+ if (presentation.artifactManifest) {
1505
+ artifactManifest = presentation.artifactManifest;
1506
+ }
1507
+ const contentWithSessionWarnings = aboutBlankSessionMismatch ? [...presentation.content] : presentation.content;
1508
+ if (aboutBlankSessionMismatch) {
1509
+ const warning = buildAboutBlankWarning(aboutBlankSessionMismatch);
1510
+ if (contentWithSessionWarnings[0]?.type === "text") {
1511
+ contentWithSessionWarnings[0] = {
1512
+ ...contentWithSessionWarnings[0],
1513
+ text: `${warning}\n\n${contentWithSessionWarnings[0].text}`,
1514
+ };
1515
+ } else {
1516
+ contentWithSessionWarnings.unshift({ type: "text", text: warning });
1517
+ }
1518
+ }
1519
+ const redactedContent = contentWithSessionWarnings.map((item) =>
1520
+ item.type === "text" ? { ...item, text: redactSensitiveText(item.text) } : item,
1521
+ );
1522
+
1523
+ return {
1524
+ content: redactedContent,
1525
+ details: {
1526
+ args: redactedArgs,
1527
+ artifactManifest: redactSensitiveValue(presentation.artifactManifest),
1528
+ artifactRetentionSummary: presentation.artifactRetentionSummary,
1529
+ artifacts: redactSensitiveValue(presentation.artifacts),
1530
+ batchFailure: redactSensitiveValue(presentation.batchFailure),
1531
+ batchSteps: redactSensitiveValue(presentation.batchSteps),
1532
+ command: executionPlan.commandInfo.command,
1533
+ compatibilityWorkaround,
1534
+ subcommand: executionPlan.commandInfo.subcommand,
1535
+ data: redactSensitiveValue(presentation.data),
1536
+ error: plainTextInspection ? undefined : redactSensitiveValue(presentationEnvelope?.error),
1537
+ inspection: plainTextInspection || undefined,
1538
+ navigationSummary: redactSensitiveValue(navigationSummary),
1539
+ aboutBlankSessionMismatch: redactSensitiveValue(aboutBlankSessionMismatch),
1540
+ openResultTabCorrection: redactSensitiveValue(openResultTabCorrection),
1541
+ effectiveArgs: redactedProcessArgs,
1542
+ exitCode: processResult.exitCode,
1543
+ fullOutputPath: parseFailureOutput.fullOutputPath ?? presentation.fullOutputPath,
1544
+ fullOutputPaths: presentation.fullOutputPaths,
1545
+ fullOutputUnavailable: parseFailureOutput.fullOutputUnavailable,
1546
+ imagePath: presentation.imagePath,
1547
+ imagePaths: presentation.imagePaths,
1548
+ parseError: plainTextInspection ? undefined : parseError,
1549
+ savedFile: redactSensitiveValue(presentation.savedFile),
1550
+ savedFilePath: presentation.savedFilePath ? redactSensitiveText(presentation.savedFilePath) : undefined,
1551
+ sessionMode,
1552
+ sessionTabCorrection: redactSensitiveValue(sessionTabCorrection),
1553
+ sessionTabTarget: redactSensitiveValue(currentSessionTabTarget),
1554
+ ...buildSessionDetailFields(executionPlan.sessionName, executionPlan.usedImplicitSession),
1555
+ sessionRecoveryHint: redactedRecoveryHint,
1556
+ startupScopedFlags: executionPlan.startupScopedFlags,
1557
+ stderr: processResult.stderr ? redactSensitiveText(processResult.stderr) : undefined,
1558
+ stdout: plainTextInspection
1559
+ ? redactSensitiveText(inspectionText ?? "")
1560
+ : parseSucceeded
1561
+ ? undefined
1562
+ : redactSensitiveText(processResult.stdout),
1563
+ summary: redactSensitiveText(presentation.summary),
1564
+ },
1565
+ isError: !succeeded,
1566
+ };
1567
+ } finally {
1568
+ if (processResult.stdoutSpillPath) {
1569
+ await rm(processResult.stdoutSpillPath, { force: true }).catch(() => undefined);
1570
+ }
1072
1571
  }
1073
- }
1572
+ };
1573
+
1574
+ return extractExplicitSessionName(params.args)
1575
+ ? runTool()
1576
+ : managedSessionExecutionQueue.run(runTool);
1074
1577
  },
1075
1578
  });
1076
1579
  }