@modelprofile.com/browser-runtime 5.5.1 → 5.7.0

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' | 'DIALOG_PENDING' | 'STALE_INPUT' | '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' | 'SUPERSEDED' | '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
@@ -13,6 +13,7 @@ const publicMessages = {
13
13
  FRAME_TOO_LARGE: 'The browser frame exceeded its limit.',
14
14
  DIALOG_PENDING: 'Respond to the website dialog before sending input.',
15
15
  STALE_INPUT: 'The input belongs to a previous document or dialog.',
16
+ SUPERSEDED: 'A newer hover input replaced this queued operation.',
16
17
  INVALID_INPUT: 'The request is invalid.',
17
18
  LOCKED: 'The browser runtime is already locked.',
18
19
  NOT_RUNNING: 'The browser runtime is not running.',
@@ -43,4 +44,4 @@ export const toBrowserRuntimeError = (error) => {
43
44
  }
44
45
  return new BrowserRuntimeError('ABORTED');
45
46
  };
46
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXJyb3JzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvZXJyb3JzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQXdCQSxNQUFNLGNBQWMsR0FBNkM7SUFDL0QsT0FBTyxFQUFFLDRCQUE0QjtJQUNyQyxvQkFBb0IsRUFBRSxzQ0FBc0M7SUFDNUQsbUJBQW1CLEVBQUUsNkRBQTZEO0lBQ2xGLElBQUksRUFBRSwrQkFBK0I7SUFDckMsa0JBQWtCLEVBQUUsNkJBQTZCO0lBQ2pELGtCQUFrQixFQUFFLDRCQUE0QjtJQUNoRCxrQkFBa0IsRUFBRSxrQ0FBa0M7SUFDdEQsa0JBQWtCLEVBQUUsNkNBQTZDO0lBQ2pFLGFBQWEsRUFBRSwrQkFBK0I7SUFDOUMsTUFBTSxFQUFFLDZDQUE2QztJQUNyRCxtQkFBbUIsRUFBRSxrQ0FBa0M7SUFDdkQsZUFBZSxFQUFFLHVDQUF1QztJQUN4RCxjQUFjLEVBQUUscURBQXFEO0lBQ3JFLFdBQVcsRUFBRSxxREFBcUQ7SUFDbEUsYUFBYSxFQUFFLHlCQUF5QjtJQUN4QyxNQUFNLEVBQUUsd0NBQXdDO0lBQ2hELFdBQVcsRUFBRSxxQ0FBcUM7SUFDbEQsY0FBYyxFQUFFLHlDQUF5QztJQUN6RCxjQUFjLEVBQUUsdUNBQXVDO0lBQ3ZELGtCQUFrQixFQUFFLHlDQUF5QztJQUM3RCxnQkFBZ0IsRUFBRSx3Q0FBd0M7SUFDMUQsT0FBTyxFQUFFLDBCQUEwQjtDQUNwQyxDQUFDO0FBRUYsTUFBTSxPQUFPLG1CQUFvQixTQUFRLEtBQUs7SUFDNUIsSUFBSSxDQUEyQjtJQUUvQyxZQUFZLElBQThCLEVBQUUsZUFBd0I7UUFDbEUsS0FBSyxDQUFDLGVBQWUsSUFBSSxjQUFjLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQztRQUMvQyxJQUFJLENBQUMsSUFBSSxHQUFHLHFCQUFxQixDQUFDO1FBQ2xDLElBQUksQ0FBQyxJQUFJLEdBQUcsSUFBSSxDQUFDO0lBQ25CLENBQUM7SUFFRCxJQUFXLGFBQWE7UUFDdEIsT0FBTyxjQUFjLENBQUMsSUFBSSxDQUFDLElBQUksQ0FBQyxDQUFDO0lBQ25DLENBQUM7Q0FDRjtBQUVELE1BQU0sQ0FBQyxNQUFNLHFCQUFxQixHQUFHLENBQUMsS0FBYyxFQUF1QixFQUFFO0lBQzNFLElBQUksS0FBSyxZQUFZLG1CQUFtQixFQUFFLENBQUM7UUFDekMsT0FBTyxLQUFLLENBQUM7SUFDZixDQUFDO0lBQ0QsSUFDRSxLQUFLLFlBQVksS0FBSztXQUNuQixDQUFDLEtBQUssQ0FBQyxJQUFJLEtBQUssWUFBWSxJQUFJLEtBQUssQ0FBQyxJQUFJLEtBQUssY0FBYyxDQUFDLEVBQ2pFLENBQUM7UUFDRCxPQUFPLElBQUksbUJBQW1CLENBQUMsS0FBSyxDQUFDLElBQUksS0FBSyxjQUFjLENBQUMsQ0FBQyxDQUFDLFNBQVMsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDLENBQUM7SUFDeEYsQ0FBQztJQUNELE9BQU8sSUFBSSxtQkFBbUIsQ0FBQyxTQUFTLENBQUMsQ0FBQztBQUM1QyxDQUFDLENBQUMifQ==
47
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXJyb3JzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvZXJyb3JzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQXlCQSxNQUFNLGNBQWMsR0FBNkM7SUFDL0QsT0FBTyxFQUFFLDRCQUE0QjtJQUNyQyxvQkFBb0IsRUFBRSxzQ0FBc0M7SUFDNUQsbUJBQW1CLEVBQUUsNkRBQTZEO0lBQ2xGLElBQUksRUFBRSwrQkFBK0I7SUFDckMsa0JBQWtCLEVBQUUsNkJBQTZCO0lBQ2pELGtCQUFrQixFQUFFLDRCQUE0QjtJQUNoRCxrQkFBa0IsRUFBRSxrQ0FBa0M7SUFDdEQsa0JBQWtCLEVBQUUsNkNBQTZDO0lBQ2pFLGFBQWEsRUFBRSwrQkFBK0I7SUFDOUMsTUFBTSxFQUFFLDZDQUE2QztJQUNyRCxtQkFBbUIsRUFBRSxrQ0FBa0M7SUFDdkQsZUFBZSxFQUFFLHVDQUF1QztJQUN4RCxjQUFjLEVBQUUscURBQXFEO0lBQ3JFLFdBQVcsRUFBRSxxREFBcUQ7SUFDbEUsVUFBVSxFQUFFLHFEQUFxRDtJQUNqRSxhQUFhLEVBQUUseUJBQXlCO0lBQ3hDLE1BQU0sRUFBRSx3Q0FBd0M7SUFDaEQsV0FBVyxFQUFFLHFDQUFxQztJQUNsRCxjQUFjLEVBQUUseUNBQXlDO0lBQ3pELGNBQWMsRUFBRSx1Q0FBdUM7SUFDdkQsa0JBQWtCLEVBQUUseUNBQXlDO0lBQzdELGdCQUFnQixFQUFFLHdDQUF3QztJQUMxRCxPQUFPLEVBQUUsMEJBQTBCO0NBQ3BDLENBQUM7QUFFRixNQUFNLE9BQU8sbUJBQW9CLFNBQVEsS0FBSztJQUM1QixJQUFJLENBQTJCO0lBRS9DLFlBQVksSUFBOEIsRUFBRSxlQUF3QjtRQUNsRSxLQUFLLENBQUMsZUFBZSxJQUFJLGNBQWMsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDO1FBQy9DLElBQUksQ0FBQyxJQUFJLEdBQUcscUJBQXFCLENBQUM7UUFDbEMsSUFBSSxDQUFDLElBQUksR0FBRyxJQUFJLENBQUM7SUFDbkIsQ0FBQztJQUVELElBQVcsYUFBYTtRQUN0QixPQUFPLGNBQWMsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUM7SUFDbkMsQ0FBQztDQUNGO0FBRUQsTUFBTSxDQUFDLE1BQU0scUJBQXFCLEdBQUcsQ0FBQyxLQUFjLEVBQXVCLEVBQUU7SUFDM0UsSUFBSSxLQUFLLFlBQVksbUJBQW1CLEVBQUUsQ0FBQztRQUN6QyxPQUFPLEtBQUssQ0FBQztJQUNmLENBQUM7SUFDRCxJQUNFLEtBQUssWUFBWSxLQUFLO1dBQ25CLENBQUMsS0FBSyxDQUFDLElBQUksS0FBSyxZQUFZLElBQUksS0FBSyxDQUFDLElBQUksS0FBSyxjQUFjLENBQUMsRUFDakUsQ0FBQztRQUNELE9BQU8sSUFBSSxtQkFBbUIsQ0FBQyxLQUFLLENBQUMsSUFBSSxLQUFLLGNBQWMsQ0FBQyxDQUFDLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxTQUFTLENBQUMsQ0FBQztJQUN4RixDQUFDO0lBQ0QsT0FBTyxJQUFJLG1CQUFtQixDQUFDLFNBQVMsQ0FBQyxDQUFDO0FBQzVDLENBQUMsQ0FBQyJ9
@@ -225,6 +225,7 @@ export interface IBrowserRuntimeEventSubscriptionOptions {
225
225
  export type TBrowserRuntimeVideoOffer = Omit<plugins.smartpuppeteer.ILiveVideoOffer, 'peerId'>;
226
226
  export interface IBrowserRuntimeState {
227
227
  videoAcceleration?: plugins.smartpuppeteer.ILiveVideoAcceleration;
228
+ videoSource?: plugins.smartpuppeteer.ILiveVideoSource;
228
229
  status: 'stopped' | 'starting' | 'running' | 'stopping';
229
230
  activeTabId: string | null;
230
231
  viewportRevision: number;
@@ -336,6 +337,8 @@ export interface IBrowserRuntimeOperationOptions {
336
337
  signal?: AbortSignal;
337
338
  timeoutMs?: number;
338
339
  onOperationStarted?(operationId: string): void;
340
+ /** Replace an adjacent, unstarted hover from this lease. Superseded calls reject with SUPERSEDED. */
341
+ coalesceHoverMoves?: boolean;
339
342
  }
340
343
  export interface IBrowserRuntimeFrameSubscription {
341
344
  close(): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelprofile.com/browser-runtime",
3
- "version": "5.5.1",
3
+ "version": "5.7.0",
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.12.2",
33
+ "@push.rocks/smartpuppeteer": "^2.13.0",
34
34
  "ipaddr.js": "^2.5.0"
35
35
  },
36
36
  "devDependencies": {
package/readme.md CHANGED
@@ -34,6 +34,19 @@ Human leases expose `openVideoPeer(options?)`, `answerVideoPeer(negotiationId, d
34
34
 
35
35
  Set `video` on `BrowserRuntime` for trusted host limits or ICE configuration. Defaults use direct connections with no external STUN/TURN service and automatic GPU support. The private capture extension is loaded without weakening ordinary page proxy, DNS, permission or WebRTC confinement. `state.videoAcceleration` and peer statistics report actual browser acceleration/encoder support.
36
36
 
37
+ `video: { backend: 'native', maxFrameRate: 30 }` selects SmartPuppeteer's Rust/NVIDIA
38
+ capture and NVENC sender. Chromium remains the default backend. Native viewers must
39
+ forward `state.videoSource` unchanged, including its RTP presentation fence and coded
40
+ frame alignment. The native viewport negotiates device scale 1 and fits the configured
41
+ capture limits while preserving the preferred aspect ratio. The returned viewport owns
42
+ input coordinates. Unsupported receiver codec limits fail negotiation explicitly.
43
+
44
+ Forward DOM pointer `event.timeStamp` as `dispatchMouse().timestampMs` to preserve the
45
+ occurrence spacing of a gesture delivered in a transport burst. Participant changes reset
46
+ the native timing history because viewer clocks have different origins; synthesized
47
+ button releases omit timestamps. Existing operation ordering and held-button ownership
48
+ remain authoritative.
49
+
37
50
  Native viewers should call `lease.subscribeEvents(listener, { includeFrames: false })`. They receive state and errors without generating JPEG traffic. Existing image consumers retain the default `includeFrames: true`; capture starts for the first such subscriber and stops after the last closes. Agent snapshots remain independent of continuous capture. Image subscription cleanup is serialized, coalesced and retryable; a failed disable is retained until cleanup succeeds or its exact browser incarnation terminates.
38
51
 
39
52
  ## Issue Reporting and Security
@@ -164,6 +177,9 @@ Agent actions are exactly `navigate`, `snapshot`, `screenshot`, `click`, `fill`,
164
177
 
165
178
  BrowserRuntime alone owns the bounded execution scheduler shared by all participants. Host transports submit operations in order without adding a second execution queue or performing asynchronous authorization ahead of admission. One statistics read may run independently of mutations; its preflight and completion cannot fence input, navigation, or dialog replies. 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.
166
179
 
180
+ Interactive hosts can pass `{ coalesceHoverMoves: true }` to `humanLease.dispatchMouse()`. Only an adjacent, unstarted, unpressed move from the same lease, document, viewport and modifier state is replaced. The replaced promise rejects with `BrowserRuntimeError` code `SUPERSEDED`; hosts should report that terminal outcome without treating it as a failed connection. Drag paths, queued or held buttons, other participants, and discrete operations retain their order. The default preserves every submitted operation.
181
+
182
+
167
183
  `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.
168
184
 
169
185
  `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.
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@modelprofile.com/browser-runtime',
6
- version: '5.5.1',
6
+ version: '5.7.0',
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
  }
@@ -172,6 +172,7 @@ export interface IBrowserRuntimeLeaseRenewalOptions {
172
172
  }
173
173
 
174
174
  interface IOperationRecord {
175
+ hoverKey?: string;
175
176
  operationId: string;
176
177
  lease: ILeaseRecord;
177
178
  controller: AbortController;
@@ -183,6 +184,7 @@ interface IOperationRecord {
183
184
  }
184
185
 
185
186
  interface IQueuedOperationRecord {
187
+ hoverKey?: string;
186
188
  operationId: string;
187
189
  lease: ILeaseRecord;
188
190
  action: string;
@@ -263,6 +265,7 @@ interface IResourceSlot {
263
265
  profileDirectory?: string;
264
266
  unsubscribeSession?: () => void;
265
267
  leases: Map<string, ILeaseRecord>;
268
+ lastMouseTimestampLeaseId?: string;
266
269
  operations: Set<IOperationRecord>;
267
270
  inputCleanup?: { session: ILiveBrowserSessionLike; incarnationGeneration: number; promise: Promise<void> };
268
271
  operationScheduler: BrowserOperationScheduler<IQueuedOperationRecord>;
@@ -1595,7 +1598,16 @@ export class BrowserRuntime {
1595
1598
  width: Math.min(...preferences.map((value) => value.width)),
1596
1599
  height: Math.min(...preferences.map((value) => value.height)),
1597
1600
  deviceScaleFactor: Math.min(...preferences.map((value) => value.deviceScaleFactor)),
1598
- } : current.viewport;
1601
+ } : { ...current.viewport };
1602
+ if (this.options.video.backend === 'native') {
1603
+ // A native desktop has physical pixels. Negotiate the accepted viewport
1604
+ // here so all viewers use the same layout and coordinate space.
1605
+ const scale = Math.min(1, this.options.video.maxWidth / viewport.width,
1606
+ this.options.video.maxHeight / viewport.height);
1607
+ viewport.width = Math.max(1, Math.floor(viewport.width * scale));
1608
+ viewport.height = Math.max(1, Math.floor(viewport.height * scale));
1609
+ viewport.deviceScaleFactor = 1;
1610
+ }
1599
1611
  if (
1600
1612
  viewport.width !== current.viewport.width || viewport.height !== current.viewport.height
1601
1613
  || viewport.deviceScaleFactor !== current.viewport.deviceScaleFactor
@@ -1671,6 +1683,21 @@ export class BrowserRuntime {
1671
1683
  operationOptions: IBrowserRuntimeOperationOptions = {},
1672
1684
  ): Promise<void> {
1673
1685
  this.requireHuman(lease);
1686
+ const mouse = action === 'dispatchMouse' ? input as plugins.smartpuppeteer.ILiveBrowserMouseInput : undefined;
1687
+ if (mouse?.timestampMs !== undefined && (!Number.isFinite(mouse.timestampMs) || mouse.timestampMs < 0)) {
1688
+ throw new BrowserRuntimeError('INVALID_INPUT');
1689
+ }
1690
+ // Drag paths and shared held input have observable intermediate positions.
1691
+ // Only explicit hover submissions can replace another pending hover.
1692
+ const hoverKey = operationOptions.coalesceHoverMoves && mouse?.type === 'move'
1693
+ && (mouse.buttons ?? 0) === 0 && (mouse.button ?? 'none') === 'none'
1694
+ && Number.isFinite(mouse.x) && Number.isFinite(mouse.y) && mouse.x >= 0 && mouse.y >= 0
1695
+ && ![...lease.slot.leases.values()].some(participant => participant.heldButtons.size > 0)
1696
+ && !lease.slot.operationScheduler.pending.some(operation => operation.action === 'dispatchMouse' && !operation.hoverKey)
1697
+ && ![...lease.slot.operations].some(operation => operation.action === 'dispatchMouse' && !operation.hoverKey)
1698
+ ? JSON.stringify([mouse.tabId, mouse.generation, mouse.viewportRevision,
1699
+ mouse.modifiers?.alt ?? false, mouse.modifiers?.control ?? false,
1700
+ mouse.modifiers?.meta ?? false, mouse.modifiers?.shift ?? false]) : undefined;
1674
1701
  await this.runOperation(lease, action, 'raw-input', operationOptions, async (_signal, session) => {
1675
1702
  if (action === 'dispatchMouse') {
1676
1703
  const mouse = input as plugins.smartpuppeteer.ILiveBrowserMouseInput;
@@ -1689,11 +1716,16 @@ export class BrowserRuntime {
1689
1716
  throw new BrowserRuntimeError('QUOTA_EXCEEDED');
1690
1717
  }
1691
1718
  if (mouse.type === 'down') lease.heldButtons.set(key, { ...mouse });
1719
+ const previousClock = lease.slot.lastMouseTimestampLeaseId;
1720
+ // Viewer performance clocks have unrelated origins. An untimed event
1721
+ // resets native gesture pacing when another participant takes over.
1692
1722
  await session.dispatchMouse({ ...mouse,
1723
+ timestampMs: previousClock && previousClock !== lease.leaseId ? undefined : mouse.timestampMs,
1693
1724
  buttons: this.sharedMouseButtons(lease.slot, mouse.tabId,
1694
1725
  mouse.type === 'up' ? { lease, key } : undefined),
1695
1726
  modifiers: this.sharedModifiers(lease.slot, mouse.tabId, mouse.modifiers),
1696
1727
  });
1728
+ lease.slot.lastMouseTimestampLeaseId = lease.leaseId;
1697
1729
  if (mouse.type === 'up') lease.heldButtons.delete(key);
1698
1730
  } else if (action === 'dispatchWheel') {
1699
1731
  const wheel = input as plugins.smartpuppeteer.ILiveBrowserWheelInput;
@@ -1726,7 +1758,7 @@ export class BrowserRuntime {
1726
1758
  } else {
1727
1759
  await session.insertText(input as plugins.smartpuppeteer.ILiveBrowserInsertTextInput);
1728
1760
  }
1729
- });
1761
+ }, undefined, false, hoverKey);
1730
1762
  }
1731
1763
 
1732
1764
  private keyOwnershipKey(input: plugins.smartpuppeteer.ILiveBrowserKeyInput): string {
@@ -2351,6 +2383,7 @@ export class BrowserRuntime {
2351
2383
  execute: (signal: AbortSignal, session: ILiveBrowserSessionLike) => Promise<T>,
2352
2384
  finalize?: () => Promise<void>,
2353
2385
  participantCleanup: 'participant' | 'subscription' | false = false,
2386
+ hoverKey?: string,
2354
2387
  ): Promise<T> {
2355
2388
  if (!operationOptions || typeof operationOptions !== 'object') {
2356
2389
  throw new BrowserRuntimeError('INVALID_INPUT');
@@ -2364,10 +2397,15 @@ export class BrowserRuntime {
2364
2397
  && typeof operationOptions.onOperationStarted !== 'function'
2365
2398
  ) throw new BrowserRuntimeError('INVALID_INPUT');
2366
2399
  if (operationOptions.signal?.aborted) throw new BrowserRuntimeError('ABORTED');
2400
+ if (operationOptions.coalesceHoverMoves !== undefined
2401
+ && typeof operationOptions.coalesceHoverMoves !== 'boolean') throw new BrowserRuntimeError('INVALID_INPUT');
2367
2402
  const slot = lease.slot;
2368
2403
  this.requireQueuedLease(lease, participantCleanup);
2369
2404
  this.assertSlotAvailable(slot);
2370
- if (!participantCleanup && (
2405
+ const previousHover = slot.operationScheduler.pending.at(-1);
2406
+ const superseded = hoverKey !== undefined && previousHover?.state === 'queued'
2407
+ && previousHover.lease === lease && previousHover.hoverKey === hoverKey ? previousHover : undefined;
2408
+ if (!participantCleanup && !superseded && (
2371
2409
  slot.operationScheduler.pending.filter((entry) => entry.lease === lease).length
2372
2410
  >= this.options.maxQueuedOperationsPerLease
2373
2411
  || slot.operationScheduler.pending.length >= this.options.maxQueuedOperationsPerLease
@@ -2385,6 +2423,7 @@ export class BrowserRuntime {
2385
2423
  ]);
2386
2424
  return new Promise<T>((resolve, reject) => {
2387
2425
  const queued: IQueuedOperationRecord = {
2426
+ hoverKey,
2388
2427
  operationId: randomId(18),
2389
2428
  lease,
2390
2429
  action,
@@ -2423,6 +2462,14 @@ export class BrowserRuntime {
2423
2462
  onQueuedAbort();
2424
2463
  return;
2425
2464
  }
2465
+ if (superseded) {
2466
+ // Replace atomically before draining; remove() would start the old tail's successor.
2467
+ const index = slot.operationScheduler.pending.indexOf(superseded);
2468
+ if (index >= 0) slot.operationScheduler.pending.splice(index, 1);
2469
+ superseded.state = 'settled';
2470
+ if (superseded.onQueuedAbort) superseded.signal.removeEventListener('abort', superseded.onQueuedAbort);
2471
+ superseded.reject(new BrowserRuntimeError('SUPERSEDED'));
2472
+ }
2426
2473
  slot.operationScheduler.enqueue(queued);
2427
2474
  });
2428
2475
  }
@@ -2471,6 +2518,7 @@ export class BrowserRuntime {
2471
2518
  timeoutController.signal,
2472
2519
  ]);
2473
2520
  operation = {
2521
+ hoverKey: queued.hoverKey,
2474
2522
  operationId: queued.operationId,
2475
2523
  lease,
2476
2524
  controller,
@@ -2890,6 +2938,7 @@ export class BrowserRuntime {
2890
2938
  private resourceState(state: plugins.smartpuppeteer.ILiveBrowserState): IBrowserRuntimeState {
2891
2939
  return {
2892
2940
  ...(state.videoAcceleration ? { videoAcceleration: { ...state.videoAcceleration } } : {}),
2941
+ ...(state.videoSource ? { videoSource: structuredClone(state.videoSource) } : {}),
2893
2942
  status: state.status,
2894
2943
  activeTabId: state.activeTabId ? truncateString(state.activeTabId, 128) : null,
2895
2944
  viewportRevision: state.viewportRevision,
package/ts/errors.ts CHANGED
@@ -13,6 +13,7 @@ export type TBrowserRuntimeErrorCode =
13
13
  | 'FRAME_TOO_LARGE'
14
14
  | 'DIALOG_PENDING'
15
15
  | 'STALE_INPUT'
16
+ | 'SUPERSEDED'
16
17
  | 'INVALID_INPUT'
17
18
  | 'LOCKED'
18
19
  | 'NOT_RUNNING'
@@ -37,6 +38,7 @@ const publicMessages: Record<TBrowserRuntimeErrorCode, string> = {
37
38
  FRAME_TOO_LARGE: 'The browser frame exceeded its limit.',
38
39
  DIALOG_PENDING: 'Respond to the website dialog before sending input.',
39
40
  STALE_INPUT: 'The input belongs to a previous document or dialog.',
41
+ SUPERSEDED: 'A newer hover input replaced this queued operation.',
40
42
  INVALID_INPUT: 'The request is invalid.',
41
43
  LOCKED: 'The browser runtime is already locked.',
42
44
  NOT_RUNNING: 'The browser runtime is not running.',
package/ts/interfaces.ts CHANGED
@@ -333,6 +333,7 @@ export type TBrowserRuntimeVideoOffer = Omit<plugins.smartpuppeteer.ILiveVideoOf
333
333
 
334
334
  export interface IBrowserRuntimeState {
335
335
  videoAcceleration?: plugins.smartpuppeteer.ILiveVideoAcceleration;
336
+ videoSource?: plugins.smartpuppeteer.ILiveVideoSource;
336
337
  status: 'stopped' | 'starting' | 'running' | 'stopping';
337
338
  activeTabId: string | null;
338
339
  viewportRevision: number;
@@ -460,6 +461,8 @@ export interface IBrowserRuntimeOperationOptions {
460
461
  signal?: AbortSignal;
461
462
  timeoutMs?: number;
462
463
  onOperationStarted?(operationId: string): void;
464
+ /** Replace an adjacent, unstarted hover from this lease. Superseded calls reject with SUPERSEDED. */
465
+ coalesceHoverMoves?: boolean;
463
466
  }
464
467
 
465
468
  export interface IBrowserRuntimeFrameSubscription {