@modelprofile.com/browser-runtime 5.3.0 → 5.4.1

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,4 +1,4 @@
1
- export type TBrowserRuntimeErrorCode = 'ABORTED' | 'AUTHORIZATION_DENIED' | 'ATTACHMENT_CONFLICT' | 'BUSY' | 'CAPABILITY_EXPIRED' | 'CAPABILITY_INVALID' | 'CAPABILITY_REVOKED' | 'CONFINEMENT_FAILED' | 'EGRESS_DENIED' | 'FENCED' | 'FRAME_STREAM_FAILED' | 'FRAME_TOO_LARGE' | 'INVALID_INPUT' | 'LOCKED' | 'NOT_RUNNING' | 'PROTOCOL_ERROR' | 'QUOTA_EXCEEDED' | 'RESOURCE_NOT_FOUND' | 'RESOURCE_RETIRED' | 'TIMEOUT';
1
+ export type TBrowserRuntimeErrorCode = 'ABORTED' | 'AUTHORIZATION_DENIED' | 'ATTACHMENT_CONFLICT' | 'BUSY' | 'CAPABILITY_EXPIRED' | 'CAPABILITY_INVALID' | 'CAPABILITY_REVOKED' | 'CONFINEMENT_FAILED' | 'EGRESS_DENIED' | 'FENCED' | 'FRAME_STREAM_FAILED' | 'FRAME_TOO_LARGE' | 'DIALOG_PENDING' | 'STALE_INPUT' | 'INVALID_INPUT' | 'LOCKED' | 'NOT_RUNNING' | 'PROTOCOL_ERROR' | 'QUOTA_EXCEEDED' | 'RESOURCE_NOT_FOUND' | 'RESOURCE_RETIRED' | 'TIMEOUT';
2
2
  export declare class BrowserRuntimeError extends Error {
3
3
  readonly code: TBrowserRuntimeErrorCode;
4
4
  constructor(code: TBrowserRuntimeErrorCode, internalMessage?: string);
package/dist_ts/errors.js CHANGED
@@ -11,6 +11,8 @@ const publicMessages = {
11
11
  FENCED: 'The browser resource is permanently fenced.',
12
12
  FRAME_STREAM_FAILED: 'The browser frame stream failed.',
13
13
  FRAME_TOO_LARGE: 'The browser frame exceeded its limit.',
14
+ DIALOG_PENDING: 'Respond to the website dialog before sending input.',
15
+ STALE_INPUT: 'The input belongs to a previous document or dialog.',
14
16
  INVALID_INPUT: 'The request is invalid.',
15
17
  LOCKED: 'The browser runtime is already locked.',
16
18
  NOT_RUNNING: 'The browser runtime is not running.',
@@ -41,4 +43,4 @@ export const toBrowserRuntimeError = (error) => {
41
43
  }
42
44
  return new BrowserRuntimeError('ABORTED');
43
45
  };
44
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXJyb3JzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvZXJyb3JzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQXNCQSxNQUFNLGNBQWMsR0FBNkM7SUFDL0QsT0FBTyxFQUFFLDRCQUE0QjtJQUNyQyxvQkFBb0IsRUFBRSxzQ0FBc0M7SUFDNUQsbUJBQW1CLEVBQUUsNkRBQTZEO0lBQ2xGLElBQUksRUFBRSwrQkFBK0I7SUFDckMsa0JBQWtCLEVBQUUsNkJBQTZCO0lBQ2pELGtCQUFrQixFQUFFLDRCQUE0QjtJQUNoRCxrQkFBa0IsRUFBRSxrQ0FBa0M7SUFDdEQsa0JBQWtCLEVBQUUsNkNBQTZDO0lBQ2pFLGFBQWEsRUFBRSwrQkFBK0I7SUFDOUMsTUFBTSxFQUFFLDZDQUE2QztJQUNyRCxtQkFBbUIsRUFBRSxrQ0FBa0M7SUFDdkQsZUFBZSxFQUFFLHVDQUF1QztJQUN4RCxhQUFhLEVBQUUseUJBQXlCO0lBQ3hDLE1BQU0sRUFBRSx3Q0FBd0M7SUFDaEQsV0FBVyxFQUFFLHFDQUFxQztJQUNsRCxjQUFjLEVBQUUseUNBQXlDO0lBQ3pELGNBQWMsRUFBRSx1Q0FBdUM7SUFDdkQsa0JBQWtCLEVBQUUseUNBQXlDO0lBQzdELGdCQUFnQixFQUFFLHdDQUF3QztJQUMxRCxPQUFPLEVBQUUsMEJBQTBCO0NBQ3BDLENBQUM7QUFFRixNQUFNLE9BQU8sbUJBQW9CLFNBQVEsS0FBSztJQUM1QixJQUFJLENBQTJCO0lBRS9DLFlBQVksSUFBOEIsRUFBRSxlQUF3QjtRQUNsRSxLQUFLLENBQUMsZUFBZSxJQUFJLGNBQWMsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDO1FBQy9DLElBQUksQ0FBQyxJQUFJLEdBQUcscUJBQXFCLENBQUM7UUFDbEMsSUFBSSxDQUFDLElBQUksR0FBRyxJQUFJLENBQUM7SUFDbkIsQ0FBQztJQUVELElBQVcsYUFBYTtRQUN0QixPQUFPLGNBQWMsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7SUFDbkMsQ0FBQztDQUNGO0FBRUQsTUFBTSxDQUFDLE1BQU0scUJBQXFCLEdBQUcsQ0FBQyxLQUFjLEVBQXVCLEVBQUU7SUFDM0UsSUFBSSxLQUFLLFlBQVksbUJBQW1CLEVBQUUsQ0FBQztRQUN6QyxPQUFPLEtBQUssQ0FBQztJQUNmLENBQUM7SUFDRCxJQUNFLEtBQUssWUFBWSxLQUFLO1dBQ25CLENBQUMsS0FBSyxDQUFDLElBQUksS0FBSyxZQUFZLElBQUksS0FBSyxDQUFDLElBQUksS0FBSyxjQUFjLENBQUMsRUFDakUsQ0FBQztRQUNELE9BQU8sSUFBSSxtQkFBbUIsQ0FBQyxLQUFLLENBQUMsSUFBSSxLQUFLLGNBQWMsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxTQUFTLENBQUMsQ0FBQztJQUN4RixDQUFDO0lBQ0QsT0FBTyxJQUFJLG1CQUFtQixDQUFDLFNBQVMsQ0FBQyxDQUFDO0FBQzVDLENBQUMsQ0FBQyJ9
46
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXJyb3JzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvZXJyb3JzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQXdCQSxNQUFNLGNBQWMsR0FBNkM7SUFDL0QsT0FBTyxFQUFFLDRCQUE0QjtJQUNyQyxvQkFBb0IsRUFBRSxzQ0FBc0M7SUFDNUQsbUJBQW1CLEVBQUUsNkRBQTZEO0lBQ2xGLElBQUksRUFBRSwrQkFBK0I7SUFDckMsa0JBQWtCLEVBQUUsNkJBQTZCO0lBQ2pELGtCQUFrQixFQUFFLDRCQUE0QjtJQUNoRCxrQkFBa0IsRUFBRSxrQ0FBa0M7SUFDdEQsa0JBQWtCLEVBQUUsNkNBQTZDO0lBQ2pFLGFBQWEsRUFBRSwrQkFBK0I7SUFDOUMsTUFBTSxFQUFFLDZDQUE2QztJQUNyRCxtQkFBbUIsRUFBRSxrQ0FBa0M7SUFDdkQsZUFBZSxFQUFFLHVDQUF1QztJQUN4RCxjQUFjLEVBQUUscURBQXFEO0lBQ3JFLFdBQVcsRUFBRSxxREFBcUQ7SUFDbEUsYUFBYSxFQUFFLHlCQUF5QjtJQUN4QyxNQUFNLEVBQUUsd0NBQXdDO0lBQ2hELFdBQVcsRUFBRSxxQ0FBcUM7SUFDbEQsY0FBYyxFQUFFLHlDQUF5QztJQUN6RCxjQUFjLEVBQUUsdUNBQXVDO0lBQ3ZELGtCQUFrQixFQUFFLHlDQUF5QztJQUM3RCxnQkFBZ0IsRUFBRSx3Q0FBd0M7SUFDMUQsT0FBTyxFQUFFLDBCQUEwQjtDQUNwQyxDQUFDO0FBRUYsTUFBTSxPQUFPLG1CQUFvQixTQUFRLEtBQUs7SUFDNUIsSUFBSSxDQUEyQjtJQUUvQyxZQUFZLElBQThCLEVBQUUsZUFBd0I7UUFDbEUsS0FBSyxDQUFDLGVBQWUsSUFBSSxjQUFjLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQztRQUMvQyxJQUFJLENBQUMsSUFBSSxHQUFHLHFCQUFxQixDQUFDO1FBQ2xDLElBQUksQ0FBQyxJQUFJLEdBQUcsSUFBSSxDQUFDO0lBQ25CLENBQUM7SUFFRCxJQUFXLGFBQWE7UUFDdEIsT0FBTyxjQUFjLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDO0lBQ25DLENBQUM7Q0FDRjtBQUVELE1BQU0sQ0FBQyxNQUFNLHFCQUFxQixHQUFHLENBQUMsS0FBYyxFQUF1QixFQUFFO0lBQzNFLElBQUksS0FBSyxZQUFZLG1CQUFtQixFQUFFLENBQUM7UUFDekMsT0FBTyxLQUFLLENBQUM7SUFDZixDQUFDO0lBQ0QsSUFDRSxLQUFLLFlBQVksS0FBSztXQUNuQixDQUFDLEtBQUssQ0FBQyxJQUFJLEtBQUssWUFBWSxJQUFJLEtBQUssQ0FBQyxJQUFJLEtBQUssY0FBYyxDQUFDLEVBQ2pFLENBQUM7UUFDRCxPQUFPLElBQUksbUJBQW1CLENBQUMsS0FBSyxDQUFDLElBQUksS0FBSyxjQUFjLENBQUMsQ0FBQyxDQUFDLFNBQVMsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDLENBQUM7SUFDeEYsQ0FBQztJQUNELE9BQU8sSUFBSSxtQkFBbUIsQ0FBQyxTQUFTLENBQUMsQ0FBQztBQUM1QyxDQUFDLENBQUMifQ==
@@ -101,7 +101,7 @@ export type TBrowserRuntimeOperationIdentity = TReadonlyBrowserCapabilityAuthori
101
101
  readonly classification: TBrowserRuntimeOperationClassification;
102
102
  readonly startedAt: number;
103
103
  };
104
- export type TBrowserRuntimeOperationClassification = 'raw-input' | 'frame-stream' | 'video-peer' | 'devtools' | 'viewport' | 'navigation' | 'tab' | 'agent-action';
104
+ export type TBrowserRuntimeOperationClassification = 'dialog' | 'raw-input' | 'frame-stream' | 'video-peer' | 'devtools' | 'viewport' | 'navigation' | 'tab' | 'agent-action';
105
105
  /** The viewport accepted by the shared resource, which may be smaller than one viewer's request. */
106
106
  export interface IBrowserRuntimeViewportResult {
107
107
  viewport: plugins.smartpuppeteer.ILiveBrowserViewport;
@@ -120,6 +120,7 @@ export type TBrowserRuntimeAuditEvent = TBrowserRuntimeOperationIdentity & {
120
120
  readonly errorCode?: string;
121
121
  };
122
122
  export interface ILiveBrowserSessionLike {
123
+ respondToDialog(input: plugins.smartpuppeteer.ILiveBrowserDialogResponse, options?: plugins.smartpuppeteer.ILiveBrowserOperationOptions): Promise<void>;
123
124
  openDevTools?(options: plugins.smartpuppeteer.ILiveBrowserDevToolsOptions, operationOptions?: plugins.smartpuppeteer.ILiveBrowserOperationOptions): Promise<plugins.smartpuppeteer.ILiveBrowserDevToolsConnection>;
124
125
  start(options?: plugins.smartpuppeteer.ILiveBrowserOperationOptions): Promise<void>;
125
126
  stop(): Promise<void>;
@@ -207,6 +208,7 @@ export interface IBrowserRuntimeOptions {
207
208
  artifacts?: Omit<IBrowserArtifactStoreOptions, 'rootDirectory'>;
208
209
  }
209
210
  export interface IBrowserRuntimeTabState {
211
+ dialog?: plugins.smartpuppeteer.ILiveBrowserDialog;
210
212
  id: string;
211
213
  url: string;
212
214
  title: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelprofile.com/browser-runtime",
3
- "version": "5.3.0",
3
+ "version": "5.4.1",
4
4
  "private": false,
5
5
  "description": "Parent-owned, resource-centric Chromium runtime with revisioned attachment fencing, authenticated human and agent control, fail-closed egress, bounded artifacts, and Flex/MCP adapters.",
6
6
  "main": "dist_ts/index.js",
@@ -30,7 +30,7 @@
30
30
  "@push.rocks/smartagent": "^6.0.0",
31
31
  "@push.rocks/smartipc": "^2.6.0",
32
32
  "@push.rocks/smartmcp": "^0.3.0",
33
- "@push.rocks/smartpuppeteer": "^2.9.0",
33
+ "@push.rocks/smartpuppeteer": "^2.12.0",
34
34
  "ipaddr.js": "^2.5.0"
35
35
  },
36
36
  "devDependencies": {
package/readme.md CHANGED
@@ -24,6 +24,10 @@ No browser debugging port or browser-wide target discovery is exposed. Pair this
24
24
  connection with `DevToolsFrontend` from `@push.rocks/smartbrowser/web` for the official
25
25
  Chrome DevTools UI.
26
26
 
27
+ ## Page dialogs
28
+
29
+ `state.tabs[].dialog` exposes the exact pending website dialog. A human lease can call `respondToDialog({ tabId, dialogId, accept, promptText? }, options?)`. This response receives the same authorization, audit, cancellation, and incarnation checks as other operations while bypassing document serialization. Agent leases cannot send this human response. A pending dialog rejects raw input with `DIALOG_PENDING`, preserving held-input tracking until it closes. Before input enters a new document, the runtime releases native keys and buttons retained from the previous document. Obsolete input and dialog identities fail with `STALE_INPUT`; they do not imply a failed browser connection.
30
+
27
31
  ## Native video viewers
28
32
 
29
33
  Human leases expose `openVideoPeer(options?)`, `answerVideoPeer(negotiationId, description, options?)`, `closeVideoPeer(options?)`, and `getVideoStatistics(options?)`. Each operation runs through the resource's bounded, authorized and audited operation queue. Peer IDs are private to the lease; offers expose the negotiation ID and exact tab/generation/viewport source identity. Agent leases cannot open media peers. Releasing or revoking a human lease closes only its peer, preserving other viewers and agent work. Failed peer cleanup remains owned for a subsequent release attempt.
@@ -158,11 +162,11 @@ by `maxCapabilities`. Hosts must stop scheduling renewal when their viewer disco
158
162
 
159
163
  Agent actions are exactly `navigate`, `snapshot`, `screenshot`, `click`, `fill`, and `press`. Agents can read or delete screenshots created by their exact lease using `readArtifact()` and `deleteArtifact()`. Human leases additionally expose tab lifecycle, viewport, raw input, frame subscription/acknowledgement and refresh, and exact-resource artifact reads/deletes. JavaScript evaluation is not public.
160
164
 
161
- All participants enter one strict resource FIFO. `maxQueuedOperationsPerLease` defaults to 128 and accepts 1 through 1,024; each participant has that bound, and the resource queue is bounded by its product with `maxCapabilitiesPerResource`. Overflow fails with `QUOTA_EXCEEDED`. Releasing a participant cancels only its queued and active work. Resource termination and shutdown cancel everyone. Started uncertain work must quiesce or the exact incarnation is terminated before operation settlement.
165
+ All participants share a bounded resource queue. Up to four wheel operations may run concurrently, with authorization and native-send admission kept in order. Other input, viewport, and semantic operations remain barriers behind preceding wheel work. Dialog replies use one interrupt slot so a paused document operation cannot block its own response. `maxQueuedOperationsPerLease` defaults to 128 and accepts 1 through 1,024; each participant has that bound, and the resource queue is bounded by its product with `maxCapabilitiesPerResource`. Overflow fails with `QUOTA_EXCEEDED`. Releasing a participant cancels only its queued and active work. Resource termination and shutdown cancel everyone. Started uncertain work must quiesce or the exact incarnation is terminated before operation settlement.
162
166
 
163
167
  `setViewport()` stores the viewer's preferred width, height and device scale factor and returns `IBrowserRuntimeViewportResult` with the effective `viewport` and `viewportRevision`. The effective values are the componentwise minima across active viewers that have supplied a preference. Unchanged effective sizes do not restart the stream. Removing a viewer recomputes the viewport in the resource FIFO. Held keys and buttons are tracked per participant; leaving releases only inputs that no other participant holds. Viewport and tab/navigation transitions clear held input before changing the target. Resource-owned departure cleanup is bounded and audited as `releaseParticipant`; it does not call the external `beforeOperation` gate after the participant has lost access.
164
168
 
165
- `beforeOperation` is an optional awaited fail-closed gate. It receives the complete immutable authority, operation/capability/lease IDs, action, classification, start time, and an `AbortSignal` after dequeue but before the browser side effect starts. Classifications are `raw-input`, `frame-stream`, `video-peer`, `viewport`, `navigation`, `tab`, and `agent-action`. Hosts that require durable attempt-before-side-effect auditing should persist the attempt there. Rejection denies the operation. The default `beforeOperationTimeoutMs` is 10,000 milliseconds and accepts values from 100 through 120,000; timeout fails with `TIMEOUT`. The terminal `audit` callback remains a best-effort `completed`/`failed` notification correlated by the same operation ID and classification and runs after bounded operation cleanup releases or fences the exact reservation.
169
+ `beforeOperation` is an optional awaited fail-closed gate. It receives the complete immutable authority, operation/capability/lease IDs, action, classification, start time, and an `AbortSignal` after dequeue but before the browser side effect starts. Classifications are `raw-input`, `dialog`, `devtools`, `frame-stream`, `video-peer`, `viewport`, `navigation`, `tab`, and `agent-action`. Hosts that require durable attempt-before-side-effect auditing should persist the attempt there. Rejection denies the operation. The default `beforeOperationTimeoutMs` is 10,000 milliseconds and accepts values from 100 through 120,000; timeout fails with `TIMEOUT`. The terminal `audit` callback remains a best-effort `completed`/`failed` notification correlated by the same operation ID and classification and runs after bounded operation cleanup releases or fences the exact reservation.
166
170
 
167
171
  `lease.getAuthority()` returns an immutable process-local lease authority containing the runtime authority ID, exported `authorityGeneration`, incarnation generation, and complete capability binding. Every snapshot also binds the exact `capabilityId` and `leaseId`. `lease.isAuthorityCurrent(authority)` performs an exact synchronous revalidation suitable for a host-owned virtual stream. These values are not durable Controller state and do not replace attachment checks against the Controller database.
168
172
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@modelprofile.com/browser-runtime',
6
- version: '5.3.0',
6
+ version: '5.4.1',
7
7
  description: 'Parent-owned, resource-centric Chromium runtime with revisioned attachment fencing, authenticated human and agent control, fail-closed egress, bounded artifacts, and Flex/MCP adapters.'
8
8
  }
@@ -262,7 +262,9 @@ interface IResourceSlot {
262
262
  profileDirectory?: string;
263
263
  unsubscribeSession?: () => void;
264
264
  leases: Map<string, ILeaseRecord>;
265
- operation?: IOperationRecord;
265
+ operations: Set<IOperationRecord>;
266
+ activeQueuedOperations: Set<IQueuedOperationRecord>;
267
+ inputCleanup?: { session: ILiveBrowserSessionLike; incarnationGeneration: number; promise: Promise<void> };
266
268
  operationQueue: IQueuedOperationRecord[];
267
269
  operationSchedulerRunning: boolean;
268
270
  frameSubscriptions: Map<string, IFrameSubscriptionRecord>;
@@ -743,6 +745,8 @@ export class BrowserRuntime {
743
745
  frameSubscriptions: new Map(),
744
746
  highestFrameSequence: 0,
745
747
  producerAcknowledgements: new Set(),
748
+ operations: new Set(),
749
+ activeQueuedOperations: new Set(),
746
750
  operationQueue: [],
747
751
  operationSchedulerRunning: false,
748
752
  lifecycleTail: Promise.resolve(),
@@ -1640,6 +1644,24 @@ export class BrowserRuntime {
1640
1644
  );
1641
1645
  }
1642
1646
 
1647
+ /** @internal */
1648
+ public async leaseRespondToDialog(lease: ILeaseRecord,
1649
+ input: plugins.smartpuppeteer.ILiveBrowserDialogResponse,
1650
+ options: IBrowserRuntimeOperationOptions = {}): Promise<void> {
1651
+ this.requireHuman(lease);
1652
+ const record = validateExactKeys(input, ['tabId', 'dialogId', 'accept', 'promptText'], 'dialog');
1653
+ const tabId = validateBoundedString(record.tabId, 'tabId', 1, 128);
1654
+ const dialogId = validateBoundedString(record.dialogId, 'dialogId', 1, 128);
1655
+ if (typeof record.accept !== 'boolean' || (record.promptText !== undefined
1656
+ && (typeof record.promptText !== 'string' || record.promptText.length > 32768))) {
1657
+ throw new BrowserRuntimeError('INVALID_INPUT');
1658
+ }
1659
+ const response = { tabId, dialogId, accept: record.accept,
1660
+ ...(record.promptText !== undefined ? { promptText: record.promptText as string } : {}) };
1661
+ return this.runOperation(lease, 'respondToDialog', 'dialog', options,
1662
+ async (signal, session) => session.respondToDialog(response, { signal }));
1663
+ }
1664
+
1643
1665
  /** @internal */
1644
1666
  public async leaseDispatchRawInput(
1645
1667
  lease: ILeaseRecord,
@@ -1722,7 +1744,7 @@ export class BrowserRuntime {
1722
1744
  const tab = state.tabs.find((value) => value.id === state.activeTabId);
1723
1745
  if (!tab || tab.status !== 'open' || tab.id !== input.tabId
1724
1746
  || tab.generation !== input.generation || state.viewportRevision !== input.viewportRevision
1725
- ) throw new BrowserRuntimeError('INVALID_INPUT');
1747
+ ) throw new BrowserRuntimeError('STALE_INPUT');
1726
1748
  }
1727
1749
 
1728
1750
  private mouseOwnershipKey(input: plugins.smartpuppeteer.ILiveBrowserMouseInput): string {
@@ -1770,6 +1792,29 @@ export class BrowserRuntime {
1770
1792
  return modifiers;
1771
1793
  }
1772
1794
 
1795
+ private async releaseStaleParticipantInput(slot: IResourceSlot, session: ILiveBrowserSessionLike): Promise<void> {
1796
+ const incarnationGeneration = slot.incarnationGeneration;
1797
+ if (slot.session !== session) throw new BrowserRuntimeError('ABORTED');
1798
+ if (slot.inputCleanup?.session === session
1799
+ && slot.inputCleanup.incarnationGeneration === incarnationGeneration) return slot.inputCleanup.promise;
1800
+ const state = session.getState();
1801
+ const tab = state.tabs.find(tab => tab.id === state.activeTabId);
1802
+ if (!tab?.streaming || tab.dialog) return;
1803
+ const affected = [...slot.leases.values()].filter(lease => [...lease.heldKeys.values(), ...lease.heldButtons.values()]
1804
+ .some(input => input.tabId !== tab.id || input.generation !== tab.generation
1805
+ || input.viewportRevision !== state.viewportRevision));
1806
+ if (!affected.length) return;
1807
+ // The runtime owns native held input, including link navigation that bypasses its command queue.
1808
+ const cleanup = { session, incarnationGeneration, promise: (async () => {
1809
+ for (const lease of affected) {
1810
+ if (slot.session !== session || slot.incarnationGeneration !== incarnationGeneration) return;
1811
+ await this.releaseParticipantInput(lease, session);
1812
+ }
1813
+ })() };
1814
+ slot.inputCleanup = cleanup;
1815
+ try { await cleanup.promise; } finally { if (slot.inputCleanup === cleanup) slot.inputCleanup = undefined; }
1816
+ }
1817
+
1773
1818
  private async releaseAllParticipantInput(
1774
1819
  slot: IResourceSlot, session: ILiveBrowserSessionLike,
1775
1820
  ): Promise<void> {
@@ -1924,10 +1969,9 @@ export class BrowserRuntime {
1924
1969
  /** @internal */
1925
1970
  public cancelLeaseOperation(lease: ILeaseRecord): boolean {
1926
1971
  this.requireValidLease(lease);
1927
- const operation = lease.slot.operation;
1928
- if (!operation || operation.lease !== lease) return false;
1929
- operation.controller.abort(new BrowserRuntimeError('ABORTED'));
1930
- return true;
1972
+ const operations = [...lease.slot.operations].filter(operation => operation.lease === lease);
1973
+ for (const operation of operations) operation.controller.abort(new BrowserRuntimeError('ABORTED'));
1974
+ return operations.length > 0;
1931
1975
  }
1932
1976
 
1933
1977
  /** @internal */
@@ -2391,37 +2435,42 @@ export class BrowserRuntime {
2391
2435
  private drainOperationQueue(slot: IResourceSlot): void {
2392
2436
  if (slot.operationSchedulerRunning) return;
2393
2437
  slot.operationSchedulerRunning = true;
2394
- const scheduler = (async () => {
2395
- while (slot.operationQueue.length > 0) {
2396
- const queued = slot.operationQueue.shift()!;
2397
- if (queued.state === 'settled') continue;
2398
- queued.state = 'starting';
2399
- try {
2400
- const result = await this.executeQueuedOperation(queued);
2401
- if ((queued as IQueuedOperationRecord).state !== 'settled') {
2402
- queued.state = 'settled';
2403
- queued.resolve(result);
2404
- }
2405
- } catch (error) {
2406
- if (queued.state !== 'settled') {
2407
- queued.state = 'settled';
2408
- queued.reject(error);
2409
- }
2410
- } finally {
2411
- if (queued.onQueuedAbort) {
2412
- queued.signal.removeEventListener('abort', queued.onQueuedAbort);
2413
- queued.onQueuedAbort = undefined;
2414
- }
2438
+ void (async () => {
2439
+ try {
2440
+ while (slot.operationQueue.length > 0) {
2441
+ // Dialog replies must unblock a page operation already waiting on Chrome.
2442
+ const dialogIndex = slot.operationQueue.findIndex(entry => entry.classification === 'dialog');
2443
+ const index = dialogIndex >= 0 ? dialogIndex : 0;
2444
+ const queued = slot.operationQueue[index]!;
2445
+ const active = [...slot.activeQueuedOperations];
2446
+ if (queued.classification === 'dialog') {
2447
+ if (active.some(entry => entry.classification === 'dialog')) break;
2448
+ } else if (active.length > 0 && !(queued.action === 'dispatchWheel'
2449
+ && active.length < 4 && active.every(entry => entry.action === 'dispatchWheel'))) break;
2450
+ slot.operationQueue.splice(index, 1);
2451
+ if (queued.state === 'settled') continue;
2452
+ queued.state = 'starting';
2453
+ slot.activeQueuedOperations.add(queued);
2454
+ let dispatched!: () => void;
2455
+ const admission = new Promise<void>(resolve => { dispatched = resolve; });
2456
+ void this.executeQueuedOperation(queued, dispatched).then(result => {
2457
+ if (queued.state !== 'settled') { queued.state = 'settled'; queued.resolve(result); }
2458
+ }, error => {
2459
+ if (queued.state !== 'settled') { queued.state = 'settled'; queued.reject(error); }
2460
+ }).finally(() => {
2461
+ dispatched();
2462
+ if (queued.onQueuedAbort) queued.signal.removeEventListener('abort', queued.onQueuedAbort);
2463
+ slot.activeQueuedOperations.delete(queued);
2464
+ this.drainOperationQueue(slot);
2465
+ });
2466
+ // Preserve authorization and native-send order without waiting for wheel ACKs.
2467
+ await admission;
2415
2468
  }
2416
- }
2417
- })().finally(() => {
2418
- slot.operationSchedulerRunning = false;
2419
- if (slot.operationQueue.length > 0) this.drainOperationQueue(slot);
2420
- });
2421
- void scheduler.catch(() => undefined);
2469
+ } finally { slot.operationSchedulerRunning = false; }
2470
+ })();
2422
2471
  }
2423
2472
 
2424
- private async executeQueuedOperation(queued: IQueuedOperationRecord): Promise<unknown> {
2473
+ private async executeQueuedOperation(queued: IQueuedOperationRecord, dispatched: () => void): Promise<unknown> {
2425
2474
  const { lease, action, classification } = queued;
2426
2475
  const slot = lease.slot;
2427
2476
  const release = await slot.mutex.acquire();
@@ -2441,7 +2490,12 @@ export class BrowserRuntime {
2441
2490
  queued.signal.throwIfAborted();
2442
2491
  this.requireQueuedLease(lease, queued.participantCleanup);
2443
2492
  this.assertSlotAvailable(slot);
2444
- if (slot.operation) throw new BrowserRuntimeError('BUSY');
2493
+ const active = [...slot.operations];
2494
+ if (classification === 'dialog' ? active.some(operation => operation.action === 'respondToDialog')
2495
+ : active.length > 0 && !(action === 'dispatchWheel' && active.length < 4
2496
+ && active.every(operation => operation.action === 'dispatchWheel'))) {
2497
+ throw new BrowserRuntimeError('BUSY');
2498
+ }
2445
2499
  session = slot.session!;
2446
2500
  generation = slot.arbitrationGeneration;
2447
2501
  incarnationGeneration = slot.incarnationGeneration;
@@ -2471,7 +2525,7 @@ export class BrowserRuntime {
2471
2525
  incarnationGeneration,
2472
2526
  promise: new Promise<void>((resolve) => { resolvePreflight = resolve; }),
2473
2527
  };
2474
- slot.operation = operation;
2528
+ slot.operations.add(operation);
2475
2529
  queued.state = 'active';
2476
2530
  if (queued.onQueuedAbort) {
2477
2531
  queued.signal.removeEventListener('abort', queued.onQueuedAbort);
@@ -2507,7 +2561,7 @@ export class BrowserRuntime {
2507
2561
  this.requireQueuedLease(lease, queued.participantCleanup);
2508
2562
  this.assertSlotAvailable(slot);
2509
2563
  if (
2510
- slot.operation !== operation
2564
+ !slot.operations.has(operation)
2511
2565
  || slot.session !== session
2512
2566
  || slot.arbitrationGeneration !== generation
2513
2567
  || slot.incarnationGeneration !== incarnationGeneration
@@ -2517,8 +2571,11 @@ export class BrowserRuntime {
2517
2571
  if (classification === 'navigation' || classification === 'tab') {
2518
2572
  await this.releaseAllParticipantInput(slot, session);
2519
2573
  }
2574
+ if (classification !== 'dialog') await this.releaseStaleParticipantInput(slot, session);
2520
2575
  combinedSignal.throwIfAborted();
2521
- return queued.execute(combinedSignal, session);
2576
+ const result = queued.execute(combinedSignal, session);
2577
+ dispatched();
2578
+ return result;
2522
2579
  })();
2523
2580
  operation.promise = executionPromise.then(() => undefined, () => undefined).finally(() => {
2524
2581
  executionSettled = true;
@@ -2548,10 +2605,11 @@ export class BrowserRuntime {
2548
2605
  clearTimeout(timeout!);
2549
2606
  queued.externalSignal?.removeEventListener('abort', onExternalAbort!);
2550
2607
  resolvePreflight();
2608
+ dispatched();
2551
2609
  const cleanupSlot = async (): Promise<void> => {
2552
2610
  const cleanupRelease = await slot.mutex.acquire();
2553
2611
  try {
2554
- if (slot.operation === operation) slot.operation = undefined;
2612
+ slot.operations.delete(operation);
2555
2613
  } finally {
2556
2614
  cleanupRelease();
2557
2615
  }
@@ -2560,7 +2618,7 @@ export class BrowserRuntime {
2560
2618
  if (combinedSignal!.aborted) {
2561
2619
  await this.enforceOperationQuiescence(operation).catch(() => undefined);
2562
2620
  }
2563
- if (slot.operation === operation) {
2621
+ if (slot.operations.has(operation)) {
2564
2622
  const settled = await waitBounded(operation.promise, this.options.quiescenceTimeoutMs);
2565
2623
  if (settled.settled) await cleanupSlot();
2566
2624
  }
@@ -2570,7 +2628,7 @@ export class BrowserRuntime {
2570
2628
  if (
2571
2629
  queued.finalize
2572
2630
  && (!executionStarted || executionSettled)
2573
- && slot.operation !== operation
2631
+ && !slot.operations.has(operation)
2574
2632
  ) {
2575
2633
  try {
2576
2634
  await queued.finalize();
@@ -2618,6 +2676,8 @@ export class BrowserRuntime {
2618
2676
  );
2619
2677
  }
2620
2678
  if (errorArg instanceof BrowserRuntimeError) return errorArg;
2679
+ if (errorArg instanceof plugins.smartpuppeteer.LiveBrowserInputStaleError) return new BrowserRuntimeError('STALE_INPUT');
2680
+ if (errorArg instanceof plugins.smartpuppeteer.LiveBrowserDialogPendingError) return new BrowserRuntimeError('DIALOG_PENDING');
2621
2681
  if (errorArg instanceof Error && errorArg.name === 'TimeoutError') {
2622
2682
  return new BrowserRuntimeError('TIMEOUT');
2623
2683
  }
@@ -2890,6 +2950,7 @@ export class BrowserRuntime {
2890
2950
  generation: tab.generation,
2891
2951
  appliedViewportRevision: tab.appliedViewportRevision,
2892
2952
  streaming: tab.streaming,
2953
+ ...(tab.dialog ? { dialog: { ...tab.dialog } } : {}),
2893
2954
  })),
2894
2955
  ...(state.lastError
2895
2956
  ? {
@@ -3257,7 +3318,7 @@ export class BrowserRuntime {
3257
3318
  lease.released = true;
3258
3319
  lease.controller.abort(new BrowserRuntimeError('CAPABILITY_REVOKED'));
3259
3320
  }
3260
- slot.operation?.controller.abort(new BrowserRuntimeError('CAPABILITY_REVOKED'));
3321
+ for (const operation of slot.operations) operation.controller.abort(new BrowserRuntimeError('CAPABILITY_REVOKED'));
3261
3322
  for (const subscription of subscribers) {
3262
3323
  try { subscription.listener(runtimeEvent); } catch { /* Producer failure owns cleanup. */ }
3263
3324
  }
@@ -3449,7 +3510,7 @@ export class BrowserRuntime {
3449
3510
  lease.controller.abort(new BrowserRuntimeError('CAPABILITY_REVOKED'));
3450
3511
  this.notifyFramedLeaseEnded(lease);
3451
3512
  }
3452
- slot.operation?.controller.abort(new BrowserRuntimeError('CAPABILITY_REVOKED'));
3513
+ for (const operation of slot.operations) operation.controller.abort(new BrowserRuntimeError('CAPABILITY_REVOKED'));
3453
3514
  } finally {
3454
3515
  release();
3455
3516
  }
@@ -3465,7 +3526,7 @@ export class BrowserRuntime {
3465
3526
  slot.leases.delete(lease.leaseId);
3466
3527
  if (lease.capability.lease === lease) lease.capability.lease = undefined;
3467
3528
  }
3468
- slot.operation = undefined;
3529
+ slot.operations.clear();
3469
3530
  slot.fencingGeneration = undefined;
3470
3531
  }
3471
3532
  } finally {
@@ -3630,18 +3691,18 @@ export class BrowserRuntime {
3630
3691
  const slot = lease.slot;
3631
3692
  if (slot.leases.get(lease.leaseId) !== lease) return;
3632
3693
  const release = await slot.mutex.acquire();
3633
- let operation: IOperationRecord | undefined;
3694
+ let operations: IOperationRecord[];
3634
3695
  try {
3635
3696
  lease.released = true;
3636
3697
  lease.controller.abort(new BrowserRuntimeError('ABORTED'));
3637
- operation = slot.operation?.lease === lease ? slot.operation : undefined;
3638
- operation?.controller.abort(new BrowserRuntimeError('ABORTED'));
3698
+ operations = [...slot.operations].filter(operation => operation.lease === lease);
3699
+ for (const operation of operations) operation.controller.abort(new BrowserRuntimeError('ABORTED'));
3639
3700
  } finally {
3640
3701
  release();
3641
3702
  }
3642
3703
  await this.closeFrameSubscriptionIfLease(lease);
3643
3704
  if (lease.devTools) await this.closeLeaseDevTools(lease, lease.devTools);
3644
- if (operation) {
3705
+ for (const operation of operations) {
3645
3706
  const settled = await waitBounded(operation.promise, this.options.quiescenceTimeoutMs);
3646
3707
  if (!settled.settled) {
3647
3708
  // An unquiescent operation belongs to the browser process, so its incarnation must end.
@@ -3689,13 +3750,12 @@ export class BrowserRuntime {
3689
3750
  }
3690
3751
 
3691
3752
  private async quiesceSlot(slot: IResourceSlot): Promise<void> {
3692
- const operation = slot.operation;
3693
- if (!operation) return;
3694
- operation.controller.abort(new BrowserRuntimeError('ABORTED'));
3695
- const settled = await waitBounded(operation.promise, this.options.quiescenceTimeoutMs);
3753
+ const operations = [...slot.operations];
3754
+ for (const operation of operations) operation.controller.abort(new BrowserRuntimeError('ABORTED'));
3755
+ const settled = await waitBounded(Promise.all(operations.map(operation => operation.promise)), this.options.quiescenceTimeoutMs);
3696
3756
  if (settled.settled) return;
3697
3757
  await this.terminateSlotSession(slot);
3698
- if (slot.operation === operation) slot.operation = undefined;
3758
+ for (const operation of operations) slot.operations.delete(operation);
3699
3759
  }
3700
3760
 
3701
3761
  private async enforceOperationQuiescence(operation: IOperationRecord): Promise<void> {
@@ -3711,7 +3771,7 @@ export class BrowserRuntime {
3711
3771
  } catch (error) {
3712
3772
  cleanupError = error;
3713
3773
  }
3714
- if (operation.lease.slot.operation !== operation) {
3774
+ if (!operation.lease.slot.operations.has(operation)) {
3715
3775
  if (cleanupError) throw cleanupError;
3716
3776
  return;
3717
3777
  }
@@ -3721,7 +3781,7 @@ export class BrowserRuntime {
3721
3781
  return;
3722
3782
  }
3723
3783
  const slot = operation.lease.slot;
3724
- if (slot.operation === operation && slot.session === operation.session
3784
+ if (slot.operations.has(operation) && slot.session === operation.session
3725
3785
  && slot.incarnationGeneration === operation.incarnationGeneration) {
3726
3786
  await this.terminateRegisteredResourceInternal(slot, false, {
3727
3787
  session: operation.session, incarnationGeneration: operation.incarnationGeneration,
@@ -3730,13 +3790,13 @@ export class BrowserRuntime {
3730
3790
  const release = await slot.mutex.acquire();
3731
3791
  try {
3732
3792
  if (
3733
- slot.operation === operation
3793
+ slot.operations.has(operation)
3734
3794
  && (
3735
3795
  slot.permanentlyFenced
3736
3796
  || slot.session !== operation.session
3737
3797
  || slot.incarnationGeneration !== operation.incarnationGeneration
3738
3798
  )
3739
- ) slot.operation = undefined;
3799
+ ) slot.operations.delete(operation);
3740
3800
  } finally {
3741
3801
  release();
3742
3802
  }
@@ -3778,6 +3838,7 @@ export class BrowserRuntime {
3778
3838
  }
3779
3839
  slot.unsubscribeSession?.();
3780
3840
  slot.unsubscribeSession = undefined;
3841
+ if (slot.inputCleanup?.session === session) slot.inputCleanup = undefined;
3781
3842
  slot.session = undefined;
3782
3843
  slot.highestFrameSequence = 0;
3783
3844
  slot.producerAcknowledgements.clear();
@@ -3862,7 +3923,7 @@ export class BrowserRuntime {
3862
3923
  lease.controller.abort(new BrowserRuntimeError('ABORTED'));
3863
3924
  if (!stopping) this.notifyFramedLeaseEnded(lease);
3864
3925
  }
3865
- slot.operation?.controller.abort(new BrowserRuntimeError('ABORTED'));
3926
+ for (const operation of slot.operations) operation.controller.abort(new BrowserRuntimeError('ABORTED'));
3866
3927
  } finally {
3867
3928
  release();
3868
3929
  }
@@ -3878,7 +3939,7 @@ export class BrowserRuntime {
3878
3939
  if (lease.capability.lease === lease) lease.capability.lease = undefined;
3879
3940
  slot.leases.delete(lease.leaseId);
3880
3941
  }
3881
- slot.operation = undefined;
3942
+ slot.operations.clear();
3882
3943
  slot.fencingGeneration = undefined;
3883
3944
  } finally {
3884
3945
  finalRelease();
@@ -3979,8 +4040,8 @@ export class BrowserRuntime {
3979
4040
  lease.controller.abort(new BrowserRuntimeError('CAPABILITY_REVOKED'));
3980
4041
  }
3981
4042
  }
3982
- if (slot.operation?.lease.role === 'agent') {
3983
- slot.operation.controller.abort(new BrowserRuntimeError('CAPABILITY_REVOKED'));
4043
+ for (const operation of slot.operations) {
4044
+ if (operation.lease.role === 'agent') operation.controller.abort(new BrowserRuntimeError('CAPABILITY_REVOKED'));
3984
4045
  }
3985
4046
  } finally {
3986
4047
  release();
@@ -4506,6 +4567,11 @@ export class BrowserRuntimeLease {
4506
4567
  return this.runtime.leaseSetViewport(this.record, viewport, operationOptions);
4507
4568
  }
4508
4569
 
4570
+ public respondToDialog(input: plugins.smartpuppeteer.ILiveBrowserDialogResponse,
4571
+ options: IBrowserRuntimeOperationOptions = {}): Promise<void> {
4572
+ return this.runtime.leaseRespondToDialog(this.record, input, options);
4573
+ }
4574
+
4509
4575
  public dispatchMouse(
4510
4576
  input: plugins.smartpuppeteer.ILiveBrowserMouseInput,
4511
4577
  operationOptions?: IBrowserRuntimeOperationOptions,
package/ts/errors.ts CHANGED
@@ -11,6 +11,8 @@ export type TBrowserRuntimeErrorCode =
11
11
  | 'FENCED'
12
12
  | 'FRAME_STREAM_FAILED'
13
13
  | 'FRAME_TOO_LARGE'
14
+ | 'DIALOG_PENDING'
15
+ | 'STALE_INPUT'
14
16
  | 'INVALID_INPUT'
15
17
  | 'LOCKED'
16
18
  | 'NOT_RUNNING'
@@ -33,6 +35,8 @@ const publicMessages: Record<TBrowserRuntimeErrorCode, string> = {
33
35
  FENCED: 'The browser resource is permanently fenced.',
34
36
  FRAME_STREAM_FAILED: 'The browser frame stream failed.',
35
37
  FRAME_TOO_LARGE: 'The browser frame exceeded its limit.',
38
+ DIALOG_PENDING: 'Respond to the website dialog before sending input.',
39
+ STALE_INPUT: 'The input belongs to a previous document or dialog.',
36
40
  INVALID_INPUT: 'The request is invalid.',
37
41
  LOCKED: 'The browser runtime is already locked.',
38
42
  NOT_RUNNING: 'The browser runtime is not running.',
package/ts/interfaces.ts CHANGED
@@ -131,6 +131,7 @@ export type TBrowserRuntimeOperationIdentity = TReadonlyBrowserCapabilityAuthori
131
131
  };
132
132
 
133
133
  export type TBrowserRuntimeOperationClassification =
134
+ | 'dialog'
134
135
  | 'raw-input'
135
136
  | 'frame-stream'
136
137
  | 'video-peer'
@@ -161,6 +162,8 @@ export type TBrowserRuntimeAuditEvent = TBrowserRuntimeOperationIdentity & {
161
162
  };
162
163
 
163
164
  export interface ILiveBrowserSessionLike {
165
+ respondToDialog(input: plugins.smartpuppeteer.ILiveBrowserDialogResponse,
166
+ options?: plugins.smartpuppeteer.ILiveBrowserOperationOptions): Promise<void>;
164
167
  openDevTools?(options: plugins.smartpuppeteer.ILiveBrowserDevToolsOptions,
165
168
  operationOptions?: plugins.smartpuppeteer.ILiveBrowserOperationOptions): Promise<plugins.smartpuppeteer.ILiveBrowserDevToolsConnection>;
166
169
  start(options?: plugins.smartpuppeteer.ILiveBrowserOperationOptions): Promise<void>;
@@ -310,6 +313,7 @@ export interface IBrowserRuntimeOptions {
310
313
  }
311
314
 
312
315
  export interface IBrowserRuntimeTabState {
316
+ dialog?: plugins.smartpuppeteer.ILiveBrowserDialog;
313
317
  id: string;
314
318
  url: string;
315
319
  title: string;