pi-agent-browser-native 0.2.12 → 0.2.13

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