@oh-my-pi/pi-coding-agent 15.2.1 → 15.2.3

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.
@@ -1,5 +1,8 @@
1
1
  <system-notice>
2
- Background job {{jobId}} has completed. Resume your work using the result below.
2
+ {{#if multiple}}{{jobs.length}} background jobs have completed. Resume your work using the results below.
3
3
 
4
- {{result}}
4
+ {{else}}Background job {{jobs.[0].jobId}} has completed. Resume your work using the result below.
5
+ {{/if}}{{#each jobs}}{{#if @root.multiple}}── Job {{this.jobId}}{{#if this.label}} ({{this.label}}){{/if}} ──
6
+ {{/if}}{{this.result}}{{#unless @last}}
7
+ {{/unless}}{{/each}}
5
8
  </system-notice>
package/src/sdk.ts CHANGED
@@ -31,7 +31,7 @@ import {
31
31
  Snowflake,
32
32
  } from "@oh-my-pi/pi-utils";
33
33
  import chalk from "chalk";
34
- import { AsyncJobManager, isBackgroundJobSupportEnabled } from "./async";
34
+ import { type AsyncJob, AsyncJobManager, isBackgroundJobSupportEnabled } from "./async";
35
35
  import { createAutoresearchExtension } from "./autoresearch";
36
36
  import { loadCapability } from "./capability";
37
37
  import { type Rule, ruleCapability, setActiveRules } from "./capability/rule";
@@ -101,7 +101,7 @@ import {
101
101
  import { AgentSession } from "./session/agent-session";
102
102
  import { resolveAuthBrokerConfig } from "./session/auth-broker-config";
103
103
  import { AuthBrokerClient, AuthStorage, RemoteAuthCredentialStore } from "./session/auth-storage";
104
- import { convertToLlm } from "./session/messages";
104
+ import { type CustomMessage, convertToLlm } from "./session/messages";
105
105
  import { SessionManager } from "./session/session-manager";
106
106
  import { closeAllConnections } from "./ssh/connection-manager";
107
107
  import { unmountAll } from "./ssh/sshfs-mount";
@@ -152,6 +152,83 @@ import { EventBus } from "./utils/event-bus";
152
152
  import { buildNamedToolChoice } from "./utils/tool-choice";
153
153
  import { buildWorkspaceTree, type WorkspaceTree } from "./workspace-tree";
154
154
 
155
+ type AsyncResultEntry = {
156
+ jobId: string;
157
+ result: string;
158
+ job: AsyncJob | undefined;
159
+ durationMs: number | undefined;
160
+ };
161
+
162
+ type AsyncResultJobDetails = {
163
+ jobId: string;
164
+ type?: "bash" | "task";
165
+ label?: string;
166
+ durationMs?: number;
167
+ };
168
+
169
+ type AsyncResultDetails = {
170
+ jobs: AsyncResultJobDetails[];
171
+ };
172
+
173
+ type McpNotificationEntry = {
174
+ serverName: string;
175
+ uri: string;
176
+ };
177
+
178
+ function buildAsyncResultBatchMessage(entries: AsyncResultEntry[]): CustomMessage<AsyncResultDetails> | null {
179
+ if (entries.length === 0) return null;
180
+ const jobs = entries.map(entry => ({
181
+ jobId: entry.jobId,
182
+ result: entry.result,
183
+ type: entry.job?.type,
184
+ label: entry.job?.label,
185
+ durationMs: entry.durationMs,
186
+ }));
187
+ const details: AsyncResultDetails = {
188
+ jobs: jobs.map(job => ({
189
+ jobId: job.jobId,
190
+ type: job.type,
191
+ label: job.label,
192
+ durationMs: job.durationMs,
193
+ })),
194
+ };
195
+ return {
196
+ role: "custom",
197
+ customType: "async-result",
198
+ content: prompt.render(asyncResultTemplate, {
199
+ multiple: jobs.length > 1,
200
+ jobs,
201
+ }),
202
+ display: true,
203
+ attribution: "agent",
204
+ details,
205
+ timestamp: Date.now(),
206
+ };
207
+ }
208
+
209
+ function buildMcpNotificationBatchMessage(entries: McpNotificationEntry[]): AgentMessage | null {
210
+ const resources: McpNotificationEntry[] = [];
211
+ const seen = new Set<string>();
212
+ for (const entry of entries) {
213
+ const key = `${entry.serverName}\0${entry.uri}`;
214
+ if (seen.has(key)) continue;
215
+ seen.add(key);
216
+ resources.push(entry);
217
+ }
218
+ if (resources.length === 0) return null;
219
+ const lines = [`[MCP notification] ${resources.length} resource(s) updated:`];
220
+ for (const resource of resources) {
221
+ lines.push(`- server="${resource.serverName}" uri=${resource.uri}`);
222
+ }
223
+ lines.push('Use read(path="mcp://<uri>") to inspect if relevant.');
224
+ return {
225
+ role: "user",
226
+ content: [{ type: "text", text: lines.join("\n") }],
227
+ attribution: "agent",
228
+ timestamp: Date.now(),
229
+ };
230
+ }
231
+
155
232
  // Types
156
233
  export interface CreateAgentSessionOptions {
157
234
  /** Working directory for project-local discovery. Default: getProjectDir() */
@@ -1035,23 +1112,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1035
1112
  const formattedResult = await formatAsyncResultForFollowUp(result);
1036
1113
  if (asyncJobManager!.isDeliverySuppressed(jobId)) return;
1037
1114
 
1038
- const message = prompt.render(asyncResultTemplate, { jobId, result: formattedResult });
1039
1115
  const durationMs = job ? Math.max(0, Date.now() - job.startTime) : undefined;
1040
- await session.sendCustomMessage(
1041
- {
1042
- customType: "async-result",
1043
- content: message,
1044
- display: true,
1045
- attribution: "agent",
1046
- details: {
1047
- jobId,
1048
- type: job?.type,
1049
- label: job?.label,
1050
- durationMs,
1051
- },
1052
- },
1053
- { deliverAs: "followUp", triggerTurn: true },
1054
- );
1116
+ session.yieldQueue.enqueue<AsyncResultEntry>("async-result", {
1117
+ jobId,
1118
+ result: formattedResult,
1119
+ job,
1120
+ durationMs,
1121
+ });
1055
1122
  },
1056
1123
  })
1057
1124
  : undefined;
@@ -1902,6 +1969,15 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1902
1969
  providerSessionId: options.providerSessionId,
1903
1970
  });
1904
1971
  hasSession = true;
1972
+ if (asyncJobManager) {
1973
+ session.yieldQueue.register<AsyncResultEntry>("async-result", {
1974
+ isStale: entry => asyncJobManager.isDeliverySuppressed(entry.jobId),
1975
+ build: buildAsyncResultBatchMessage,
1976
+ });
1977
+ }
1978
+ session.yieldQueue.register<McpNotificationEntry>("mcp-notification", {
1979
+ build: buildMcpNotificationBatchMessage,
1980
+ });
1905
1981
 
1906
1982
  // Attach the live session to the pre-registered ref so peers can route IRC
1907
1983
  // messages here. Refresh sessionFile in case it was unavailable at pre-register
@@ -2036,9 +2112,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2036
2112
  notificationDebounceTimers.delete(key);
2037
2113
  // Re-check: user may have disabled notifications during the debounce window
2038
2114
  if (!settings.get("mcp.notifications")) return;
2039
- void session.followUp(
2040
- `[MCP notification] Server "${serverName}" reports resource \`${uri}\` was updated. Use read(path="mcp://${uri}") to inspect if relevant.`,
2041
- );
2115
+ session.yieldQueue.enqueue<McpNotificationEntry>("mcp-notification", { serverName, uri });
2042
2116
  }, debounceMs),
2043
2117
  );
2044
2118
  });
@@ -76,6 +76,7 @@ import {
76
76
  } from "@oh-my-pi/pi-ai";
77
77
  import { MacOSPowerAssertion } from "@oh-my-pi/pi-natives";
78
78
  import {
79
+ extractRetryHint,
79
80
  getAgentDbPath,
80
81
  isEnoent,
81
82
  isUnexpectedSocketCloseMessage,
@@ -204,6 +205,7 @@ import type {
204
205
  } from "./session-manager";
205
206
  import { getLatestCompactionEntry } from "./session-manager";
206
207
  import { ToolChoiceQueue } from "./tool-choice-queue";
208
+ import { YieldQueue } from "./yield-queue";
207
209
 
208
210
  /** Session-specific events that extend the core AgentEvent */
209
211
  export type AgentSessionEvent =
@@ -734,6 +736,7 @@ export class AgentSession {
734
736
  readonly agent: Agent;
735
737
  readonly sessionManager: SessionManager;
736
738
  readonly settings: Settings;
739
+ readonly yieldQueue: YieldQueue;
737
740
 
738
741
  #powerAssertion: MacOSPowerAssertion | undefined;
739
742
 
@@ -1030,6 +1033,24 @@ export class AgentSession {
1030
1033
  };
1031
1034
  this.agent.setProviderResponseInterceptor(this.#onResponse);
1032
1035
  this.agent.setRawSseEventInterceptor(this.#onSseEvent);
1036
+ this.yieldQueue = new YieldQueue({
1037
+ isStreaming: () => this.isStreaming,
1038
+ injectStreaming: message => this.agent.followUp(message),
1039
+ injectIdle: async messages => {
1040
+ const first = messages[0];
1041
+ if (!first) return;
1042
+ await this.agent.prompt(messages.length === 1 ? first : messages);
1043
+ },
1044
+ scheduleIdleFlush: run => {
1045
+ this.#schedulePostPromptTask(
1046
+ async () => {
1047
+ await run();
1048
+ },
1049
+ { delayMs: 1 },
1050
+ );
1051
+ },
1052
+ });
1053
+ this.agent.setOnBeforeYield(() => this.yieldQueue.flush("streaming"));
1033
1054
  this.#convertToLlm = config.convertToLlm ?? convertToLlm;
1034
1055
  this.#rebuildSystemPrompt = config.rebuildSystemPrompt;
1035
1056
  this.#getMcpServerInstructions = config.getMcpServerInstructions;
@@ -2719,6 +2740,8 @@ export class AgentSession {
2719
2740
  async dispose(): Promise<void> {
2720
2741
  this.#isDisposed = true;
2721
2742
  this.#pendingBackgroundExchanges = [];
2743
+ this.yieldQueue.clear();
2744
+ this.agent.setOnBeforeYield(undefined);
2722
2745
  this.#evalExecutionDisposing = true;
2723
2746
  try {
2724
2747
  if (this.#extensionRunner?.hasHandlers("session_shutdown")) {
@@ -6949,6 +6972,11 @@ export class AgentSession {
6949
6972
  }
6950
6973
  }
6951
6974
 
6975
+ const retryHintMs = extractRetryHint(undefined, errorMessage);
6976
+ if (retryHintMs !== undefined) {
6977
+ return retryHintMs;
6978
+ }
6979
+
6952
6980
  const resetMsMatch = /x-ratelimit-reset-ms\s*[:=]\s*(\d+)/i.exec(errorMessage);
6953
6981
  if (resetMsMatch) {
6954
6982
  const resetMs = Number(resetMsMatch[1]);
@@ -0,0 +1,155 @@
1
+ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
2
+ import { logger } from "@oh-my-pi/pi-utils";
3
+
4
+ export interface YieldDispatcher<P> {
5
+ /** Drop entries already delivered through another path. Called per-entry at flush time. */
6
+ isStale?(entry: P): boolean;
7
+ /** Produce one batched AgentMessage from non-stale entries. Return null to skip. */
8
+ build(survivors: P[]): AgentMessage | null;
9
+ }
10
+
11
+ export interface YieldQueueOptions {
12
+ isStreaming: () => boolean;
13
+ injectStreaming(msg: AgentMessage): void;
14
+ injectIdle(messages: AgentMessage[]): Promise<void>;
15
+ scheduleIdleFlush(run: () => Promise<void>): void;
16
+ }
17
+
18
+ type YieldFlushMode = "streaming" | "idle";
19
+
20
+ interface StoredDispatcher {
21
+ isStale?: (entry: unknown) => boolean;
22
+ build: (survivors: unknown[]) => AgentMessage | null;
23
+ }
24
+
25
+ function formatError(error: unknown): string {
26
+ return error instanceof Error ? error.message : String(error);
27
+ }
28
+
29
+ export class YieldQueue {
30
+ readonly #options: YieldQueueOptions;
31
+ readonly #dispatchers = new Map<string, StoredDispatcher>();
32
+ readonly #entries = new Map<string, unknown[]>();
33
+ #idleFlushPending = false;
34
+
35
+ constructor(options: YieldQueueOptions) {
36
+ this.#options = options;
37
+ }
38
+
39
+ register<P>(kind: string, dispatcher: YieldDispatcher<P>): () => void {
40
+ const stored: StoredDispatcher = {
41
+ ...(dispatcher.isStale ? { isStale: entry => dispatcher.isStale?.(entry as P) ?? false } : {}),
42
+ build: survivors => dispatcher.build(survivors as P[]),
43
+ };
44
+ this.#dispatchers.set(kind, stored);
45
+ return () => {
46
+ if (this.#dispatchers.get(kind) !== stored) return;
47
+ this.#dispatchers.delete(kind);
48
+ this.#entries.delete(kind);
49
+ };
50
+ }
51
+
52
+ enqueue<P>(kind: string, entry: P): void {
53
+ if (!this.#dispatchers.has(kind)) {
54
+ logger.warn("Yield queue entry ignored for unregistered kind", { kind });
55
+ return;
56
+ }
57
+ let entries = this.#entries.get(kind);
58
+ if (!entries) {
59
+ entries = [];
60
+ this.#entries.set(kind, entries);
61
+ }
62
+ entries.push(entry);
63
+ if (!this.#options.isStreaming()) {
64
+ this.#scheduleIdleFlush();
65
+ }
66
+ }
67
+
68
+ has(kind?: string): boolean {
69
+ if (kind !== undefined) return (this.#entries.get(kind)?.length ?? 0) > 0;
70
+ for (const entries of this.#entries.values()) {
71
+ if (entries.length > 0) return true;
72
+ }
73
+ return false;
74
+ }
75
+
76
+ async flush(mode: YieldFlushMode): Promise<void> {
77
+ if (mode === "idle") {
78
+ this.#idleFlushPending = false;
79
+ }
80
+ const idleMessages: AgentMessage[] = [];
81
+ for (const [kind, dispatcher] of this.#dispatchers) {
82
+ const entries = this.#drain(kind);
83
+ if (entries.length === 0) continue;
84
+ const message = this.#build(kind, dispatcher, entries);
85
+ if (!message) continue;
86
+ if (mode === "streaming") {
87
+ try {
88
+ this.#options.injectStreaming(message);
89
+ } catch (error) {
90
+ logger.warn("Yield queue streaming dispatch failed", { kind, error: formatError(error) });
91
+ }
92
+ } else {
93
+ idleMessages.push(message);
94
+ }
95
+ }
96
+ if (mode === "idle" && idleMessages.length > 0) {
97
+ try {
98
+ await this.#options.injectIdle(idleMessages);
99
+ } catch (error) {
100
+ logger.warn("Yield queue idle dispatch failed", { error: formatError(error) });
101
+ }
102
+ }
103
+ }
104
+
105
+ clear(): void {
106
+ this.#entries.clear();
107
+ this.#idleFlushPending = false;
108
+ }
109
+
110
+ #scheduleIdleFlush(): void {
111
+ if (this.#idleFlushPending) return;
112
+ this.#idleFlushPending = true;
113
+ try {
114
+ this.#options.scheduleIdleFlush(async () => {
115
+ this.#idleFlushPending = false;
116
+ if (this.#options.isStreaming()) return;
117
+ await this.flush("idle");
118
+ });
119
+ } catch (error) {
120
+ this.#idleFlushPending = false;
121
+ logger.warn("Yield queue idle flush scheduling failed", { error: formatError(error) });
122
+ }
123
+ }
124
+
125
+ #drain(kind: string): unknown[] {
126
+ const entries = this.#entries.get(kind);
127
+ if (!entries || entries.length === 0) return [];
128
+ this.#entries.delete(kind);
129
+ return entries;
130
+ }
131
+
132
+ #build(kind: string, dispatcher: StoredDispatcher, entries: unknown[]): AgentMessage | null {
133
+ const survivors: unknown[] = [];
134
+ for (const entry of entries) {
135
+ if (dispatcher.isStale) {
136
+ let stale: boolean;
137
+ try {
138
+ stale = dispatcher.isStale(entry);
139
+ } catch (error) {
140
+ logger.warn("Yield queue stale check failed", { kind, error: formatError(error) });
141
+ continue;
142
+ }
143
+ if (stale) continue;
144
+ }
145
+ survivors.push(entry);
146
+ }
147
+ if (survivors.length === 0) return null;
148
+ try {
149
+ return dispatcher.build(survivors);
150
+ } catch (error) {
151
+ logger.warn("Yield queue build failed", { kind, error: formatError(error) });
152
+ return null;
153
+ }
154
+ }
155
+ }
@@ -13,7 +13,7 @@ export function formatDuration(ms: number): string {
13
13
  return `${days}d`;
14
14
  }
15
15
 
16
- type ProgressBarTheme = Pick<Theme, "bold" | "fg">;
16
+ type ProgressBarTheme = Pick<Theme, "bold" | "fg" | "getFgAnsi">;
17
17
 
18
18
  const unstyledProgressBarTheme: ProgressBarTheme = {
19
19
  fg(_color, text) {
@@ -22,6 +22,9 @@ const unstyledProgressBarTheme: ProgressBarTheme = {
22
22
  bold(text) {
23
23
  return text;
24
24
  },
25
+ getFgAnsi() {
26
+ return "";
27
+ },
25
28
  };
26
29
 
27
30
  function resolveProgressBarTheme(uiTheme: ProgressBarTheme | undefined): ProgressBarTheme {
@@ -3,7 +3,7 @@ import * as fs from "node:fs/promises";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
5
  import * as natives from "@oh-my-pi/pi-natives";
6
- import { getWorktreeDir, logger, Snowflake } from "@oh-my-pi/pi-utils";
6
+ import { getWorktreeDir, hashPath, logger, Snowflake } from "@oh-my-pi/pi-utils";
7
7
  import * as git from "../utils/git";
8
8
 
9
9
  const { IsoBackendKind } = natives;
@@ -26,10 +26,6 @@ export interface WorktreeBaseline {
26
26
  nested: Array<{ relativePath: string; baseline: RepoBaseline }>;
27
27
  }
28
28
 
29
- export function getEncodedProjectName(cwd: string): string {
30
- return `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
31
- }
32
-
33
29
  export async function getRepoRoot(cwd: string): Promise<string> {
34
30
  const repoRoot = await git.repo.root(cwd);
35
31
  if (!repoRoot) {
@@ -316,8 +312,7 @@ export async function ensureIsolation(
316
312
  preferred?: IsoBackendKind,
317
313
  ): Promise<IsolationHandle> {
318
314
  const repoRoot = await getRepoRoot(baseCwd);
319
- const encodedProject = getEncodedProjectName(repoRoot);
320
- const baseDir = getWorktreeDir(encodedProject, id);
315
+ const baseDir = getWorktreeDir(`${id}-${hashPath(repoRoot)}`);
321
316
  const mergedDir = path.join(baseDir, "merged");
322
317
 
323
318
  const resolution = natives.isoResolve(preferred ?? null);
@@ -540,24 +540,24 @@ async function withSoftTimeout<T>(promise: Promise<T>, timeoutMs: number, label:
540
540
  }
541
541
  }
542
542
 
543
- async function injectStealthScripts(page: Page): Promise<void> {
544
- const scripts = [
545
- stealthTamperingScript,
546
- stealthActivityScript,
547
- stealthHairlineScript,
548
- stealthBotdScript,
549
- stealthIframeScript,
550
- stealthWebglScript,
551
- stealthScreenScript,
552
- stealthFontsScript,
553
- stealthAudioScript,
554
- stealthLocaleScript,
555
- stealthPluginsScript,
556
- stealthHardwareScript,
557
- stealthCodecsScript,
558
- stealthWorkerScript,
559
- ];
543
+ const STEALTH_PATCH_SCRIPTS = [
544
+ stealthTamperingScript,
545
+ stealthActivityScript,
546
+ stealthHairlineScript,
547
+ stealthBotdScript,
548
+ stealthIframeScript,
549
+ stealthWebglScript,
550
+ stealthScreenScript,
551
+ stealthFontsScript,
552
+ stealthAudioScript,
553
+ stealthLocaleScript,
554
+ stealthPluginsScript,
555
+ stealthHardwareScript,
556
+ stealthCodecsScript,
557
+ stealthWorkerScript,
558
+ ];
560
559
 
560
+ function buildStealthInjectionScript(scripts: readonly string[] = STEALTH_PATCH_SCRIPTS): string {
561
561
  const joint = scripts
562
562
  .map(
563
563
  script => `
@@ -568,43 +568,55 @@ async function injectStealthScripts(page: Page): Promise<void> {
568
568
  )
569
569
  .join(";\n");
570
570
 
571
- await page.evaluateOnNewDocument(`(() => {
571
+ return `(() => {
572
572
  // Native function cache - captured before any tampering
573
573
  const iframe = document.createElement("iframe");
574
574
  iframe.style.display = "none";
575
- document.head.appendChild(iframe);
576
- const nativeWindow = iframe.contentWindow;
577
- if (!nativeWindow) return;
578
-
579
- // Cache pristine native functions
580
- const Function_toString = nativeWindow.Function.prototype.toString;
581
- const Object_getOwnPropertyDescriptor = nativeWindow.Object.getOwnPropertyDescriptor;
582
- const Object_getOwnPropertyDescriptors = nativeWindow.Object.getOwnPropertyDescriptors;
583
- const Object_getPrototypeOf = nativeWindow.Object.getPrototypeOf;
584
- const Object_defineProperty = nativeWindow.Object.defineProperty;
585
- const Object_getOwnPropertyDescriptorOriginal = nativeWindow.Object.getOwnPropertyDescriptor;
586
- const Object_create = nativeWindow.Object.create;
587
- const Object_keys = nativeWindow.Object.keys;
588
- const Object_getOwnPropertyNames = nativeWindow.Object.getOwnPropertyNames;
589
- const Object_entries = nativeWindow.Object.entries;
590
- const Object_setPrototypeOf = nativeWindow.Object.setPrototypeOf;
591
- const Object_assign = nativeWindow.Object.assign;
592
- const Window_setTimeout = nativeWindow.setTimeout;
593
- const Math_random = nativeWindow.Math.random;
594
- const Math_floor = nativeWindow.Math.floor;
595
- const Math_max = nativeWindow.Math.max;
596
- const Math_min = nativeWindow.Math.min;
597
- const Window_Event = nativeWindow.Event;
598
- const Promise_resolve = nativeWindow.Promise.resolve.bind(nativeWindow.Promise);
599
- const Window_Blob = nativeWindow.Blob;
600
- const Window_Proxy = nativeWindow.Proxy;
601
- const Intl_DateTimeFormat = nativeWindow.Intl.DateTimeFormat;
602
- const Date_constructor = nativeWindow.Date;
603
-
604
-
605
- ${joint}
606
-
607
- document.head.removeChild(iframe);})();`);
575
+ const container = document.head ?? document.documentElement;
576
+ if (!container) return;
577
+ container.appendChild(iframe);
578
+ try {
579
+ const nativeWindow = iframe.contentWindow;
580
+ if (!nativeWindow) return;
581
+
582
+ // Cache pristine native functions
583
+ const Function_toString = nativeWindow.Function.prototype.toString;
584
+ const Object_getOwnPropertyDescriptor = nativeWindow.Object.getOwnPropertyDescriptor;
585
+ const Object_getOwnPropertyDescriptors = nativeWindow.Object.getOwnPropertyDescriptors;
586
+ const Object_getPrototypeOf = nativeWindow.Object.getPrototypeOf;
587
+ const Object_defineProperty = nativeWindow.Object.defineProperty;
588
+ const Object_getOwnPropertyDescriptorOriginal = nativeWindow.Object.getOwnPropertyDescriptor;
589
+ const Object_create = nativeWindow.Object.create;
590
+ const Object_keys = nativeWindow.Object.keys;
591
+ const Object_getOwnPropertyNames = nativeWindow.Object.getOwnPropertyNames;
592
+ const Object_entries = nativeWindow.Object.entries;
593
+ const Object_setPrototypeOf = nativeWindow.Object.setPrototypeOf;
594
+ const Object_assign = nativeWindow.Object.assign;
595
+ const Window_setTimeout = nativeWindow.setTimeout;
596
+ const Math_random = nativeWindow.Math.random;
597
+ const Math_floor = nativeWindow.Math.floor;
598
+ const Math_max = nativeWindow.Math.max;
599
+ const Math_min = nativeWindow.Math.min;
600
+ const Window_Event = nativeWindow.Event;
601
+ const Promise_resolve = nativeWindow.Promise.resolve.bind(nativeWindow.Promise);
602
+ const Window_Blob = nativeWindow.Blob;
603
+ const Window_Proxy = nativeWindow.Proxy;
604
+ const Intl_DateTimeFormat = nativeWindow.Intl.DateTimeFormat;
605
+ const Date_constructor = nativeWindow.Date;
606
+
607
+ ${joint}
608
+ } finally {
609
+ if (iframe.parentNode) iframe.parentNode.removeChild(iframe);
610
+ }})();`;
611
+ }
612
+
613
+ async function injectStealthScripts(page: Page): Promise<void> {
614
+ await page.evaluateOnNewDocument(buildStealthInjectionScript());
615
+ }
616
+
617
+ /** Builds the browser-page stealth bootstrap source for regression tests. */
618
+ export function buildStealthInjectionScriptForTest(scripts: readonly string[] = STEALTH_PATCH_SCRIPTS): string {
619
+ return buildStealthInjectionScript(scripts);
608
620
  }
609
621
 
610
622
  /** Apply stealth patches + UA override to a headless page. Idempotent within a tab. */