@modelprofile.com/browser-runtime 2.1.1 → 3.0.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.
- package/changelog.md +17 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexprovider.js +10 -10
- package/dist_ts/classes.framed.d.ts +3 -0
- package/dist_ts/classes.framed.js +16 -3
- package/dist_ts/classes.runtime.d.ts +47 -6
- package/dist_ts/classes.runtime.js +433 -122
- package/dist_ts/index.d.ts +1 -1
- package/dist_ts/interfaces.d.ts +15 -0
- package/package.json +3 -3
- package/readme.hints.md +9 -5
- package/readme.md +11 -5
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexprovider.ts +10 -11
- package/ts/classes.framed.ts +16 -2
- package/ts/classes.runtime.ts +579 -149
- package/ts/index.ts +2 -0
- package/ts/interfaces.ts +22 -0
package/ts/classes.runtime.ts
CHANGED
|
@@ -19,6 +19,8 @@ import type {
|
|
|
19
19
|
IBrowserConfinementProbeContext,
|
|
20
20
|
IBrowserObservationResult,
|
|
21
21
|
TBrowserRuntimeAuditEvent,
|
|
22
|
+
TBrowserRuntimeLeaseAuthority,
|
|
23
|
+
TBrowserRuntimeOperationClassification,
|
|
22
24
|
TBrowserRuntimeOperationIdentity,
|
|
23
25
|
IBrowserRuntimeFrameSubscription,
|
|
24
26
|
IBrowserRuntimeOperationOptions,
|
|
@@ -45,6 +47,7 @@ import {
|
|
|
45
47
|
randomId,
|
|
46
48
|
truncateString,
|
|
47
49
|
validateBoundedString,
|
|
50
|
+
validateExactKeys,
|
|
48
51
|
validateInteger,
|
|
49
52
|
validateOptionalInteger,
|
|
50
53
|
waitBounded,
|
|
@@ -73,14 +76,17 @@ interface INormalizedRuntimeOptions {
|
|
|
73
76
|
authorizationTimeoutMs: number;
|
|
74
77
|
beforeOperationTimeoutMs: number;
|
|
75
78
|
maxTabsPerResource: number;
|
|
79
|
+
maxQueuedOperationsPerLease: number;
|
|
76
80
|
operationTimeoutMs: number;
|
|
77
81
|
quiescenceTimeoutMs: number;
|
|
78
82
|
terminationGraceMs: number;
|
|
79
83
|
terminationForceMs: number;
|
|
84
|
+
cleanupTimeoutMs: number;
|
|
80
85
|
auditTimeoutMs: number;
|
|
81
86
|
confinementProbeTimeoutMs: number;
|
|
82
87
|
idleTerminationMs: number;
|
|
83
88
|
maxFrameBytes: number;
|
|
89
|
+
maxOutstandingFrames: number;
|
|
84
90
|
frameAcknowledgementTimeoutMs: number;
|
|
85
91
|
egress: NonNullable<IBrowserRuntimeOptions['egress']>;
|
|
86
92
|
beforeLeasePublication?: IBrowserRuntimeTestingOptions['beforeLeasePublication'];
|
|
@@ -112,6 +118,7 @@ interface ILeaseRecord {
|
|
|
112
118
|
role: TBrowserActorRole;
|
|
113
119
|
slot: IResourceSlot;
|
|
114
120
|
controller: AbortController;
|
|
121
|
+
authorityGeneration: number;
|
|
115
122
|
released: boolean;
|
|
116
123
|
releasePromise?: Promise<void>;
|
|
117
124
|
releaseGeneration?: number;
|
|
@@ -129,13 +136,39 @@ interface IOperationRecord {
|
|
|
129
136
|
promise: Promise<void>;
|
|
130
137
|
}
|
|
131
138
|
|
|
139
|
+
interface IQueuedOperationRecord {
|
|
140
|
+
operationId: string;
|
|
141
|
+
lease: ILeaseRecord;
|
|
142
|
+
action: string;
|
|
143
|
+
classification: TBrowserRuntimeOperationClassification;
|
|
144
|
+
timeoutMs: number;
|
|
145
|
+
externalSignal?: AbortSignal;
|
|
146
|
+
onOperationStarted?: (operationId: string) => void;
|
|
147
|
+
signal: AbortSignal;
|
|
148
|
+
execute: (signal: AbortSignal, session: ILiveBrowserSessionLike) => Promise<unknown>;
|
|
149
|
+
resolve(value: unknown): void;
|
|
150
|
+
reject(error: unknown): void;
|
|
151
|
+
state: 'queued' | 'starting' | 'active' | 'settled';
|
|
152
|
+
onQueuedAbort?: () => void;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
interface IOutstandingFrameRecord {
|
|
156
|
+
acknowledgement: plugins.smartpuppeteer.ILiveBrowserFrameAcknowledgementRequest;
|
|
157
|
+
timer: ReturnType<typeof setTimeout>;
|
|
158
|
+
session: ILiveBrowserSessionLike;
|
|
159
|
+
incarnationGeneration: number;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
type TFrameAcknowledgementOutcome =
|
|
163
|
+
| { status: 'fulfilled'; accepted: boolean }
|
|
164
|
+
| { status: 'rejected' }
|
|
165
|
+
| { status: 'timedOut' };
|
|
166
|
+
|
|
132
167
|
interface IFrameSubscriptionRecord {
|
|
133
168
|
lease: ILeaseRecord;
|
|
134
169
|
listener: (event: TBrowserRuntimeEvent) => void;
|
|
135
|
-
outstanding
|
|
136
|
-
|
|
137
|
-
timer: ReturnType<typeof setTimeout>;
|
|
138
|
-
};
|
|
170
|
+
outstanding: Map<string, IOutstandingFrameRecord>;
|
|
171
|
+
highestSequence: number;
|
|
139
172
|
closed: boolean;
|
|
140
173
|
}
|
|
141
174
|
|
|
@@ -147,6 +180,7 @@ interface IResourceSlot {
|
|
|
147
180
|
mutex: TransitionMutex;
|
|
148
181
|
arbitrationGeneration: number;
|
|
149
182
|
incarnationGeneration: number;
|
|
183
|
+
authorityGeneration: number;
|
|
150
184
|
fencingGeneration?: number;
|
|
151
185
|
permanentlyFenced: boolean;
|
|
152
186
|
retirementPending: boolean;
|
|
@@ -158,6 +192,8 @@ interface IResourceSlot {
|
|
|
158
192
|
unsubscribeSession?: () => void;
|
|
159
193
|
lease?: ILeaseRecord;
|
|
160
194
|
operation?: IOperationRecord;
|
|
195
|
+
operationQueue: IQueuedOperationRecord[];
|
|
196
|
+
operationSchedulerRunning: boolean;
|
|
161
197
|
frameSubscription?: IFrameSubscriptionRecord;
|
|
162
198
|
idleTimer?: ReturnType<typeof setTimeout>;
|
|
163
199
|
lifecycleTail: Promise<void>;
|
|
@@ -182,6 +218,7 @@ export class BrowserRuntime {
|
|
|
182
218
|
private readonly artifactRoot: string;
|
|
183
219
|
private readonly lockPath: string;
|
|
184
220
|
private readonly artifactStore: BrowserArtifactStore;
|
|
221
|
+
private readonly runtimeAuthorityId = randomId(18);
|
|
185
222
|
private readonly slots = new Map<string, IResourceSlot>();
|
|
186
223
|
private readonly retiredResourceIds = new Uint8Array(64 * 1024);
|
|
187
224
|
private readonly retiredResourceKeys = new Uint8Array(64 * 1024);
|
|
@@ -195,6 +232,7 @@ export class BrowserRuntime {
|
|
|
195
232
|
private lockHandleClosed = false;
|
|
196
233
|
private startPromise?: Promise<void>;
|
|
197
234
|
private stopPromise?: Promise<void>;
|
|
235
|
+
private stopCleanupPromise?: Promise<void>;
|
|
198
236
|
private lifecycleState: 'stopped' | 'starting' | 'running' | 'stopping' = 'stopped';
|
|
199
237
|
private lifecycleEpoch = 0;
|
|
200
238
|
private lifecycleController = new AbortController();
|
|
@@ -331,6 +369,13 @@ export class BrowserRuntime {
|
|
|
331
369
|
256,
|
|
332
370
|
32,
|
|
333
371
|
),
|
|
372
|
+
maxQueuedOperationsPerLease: validateOptionalInteger(
|
|
373
|
+
options.maxQueuedOperationsPerLease,
|
|
374
|
+
'maxQueuedOperationsPerLease',
|
|
375
|
+
1,
|
|
376
|
+
1024,
|
|
377
|
+
128,
|
|
378
|
+
),
|
|
334
379
|
operationTimeoutMs: validateOptionalInteger(
|
|
335
380
|
options.operationTimeoutMs,
|
|
336
381
|
'operationTimeoutMs',
|
|
@@ -359,6 +404,13 @@ export class BrowserRuntime {
|
|
|
359
404
|
60_000,
|
|
360
405
|
5000,
|
|
361
406
|
),
|
|
407
|
+
cleanupTimeoutMs: validateOptionalInteger(
|
|
408
|
+
options.cleanupTimeoutMs,
|
|
409
|
+
'cleanupTimeoutMs',
|
|
410
|
+
100,
|
|
411
|
+
120_000,
|
|
412
|
+
30_000,
|
|
413
|
+
),
|
|
362
414
|
auditTimeoutMs: validateOptionalInteger(
|
|
363
415
|
options.auditTimeoutMs,
|
|
364
416
|
'auditTimeoutMs',
|
|
@@ -387,6 +439,13 @@ export class BrowserRuntime {
|
|
|
387
439
|
16 * 1024 * 1024,
|
|
388
440
|
4 * 1024 * 1024,
|
|
389
441
|
),
|
|
442
|
+
maxOutstandingFrames: validateOptionalInteger(
|
|
443
|
+
options.maxOutstandingFrames,
|
|
444
|
+
'maxOutstandingFrames',
|
|
445
|
+
1,
|
|
446
|
+
32,
|
|
447
|
+
4,
|
|
448
|
+
),
|
|
390
449
|
frameAcknowledgementTimeoutMs: validateOptionalInteger(
|
|
391
450
|
options.frameAcknowledgementTimeoutMs,
|
|
392
451
|
'frameAcknowledgementTimeoutMs',
|
|
@@ -413,7 +472,7 @@ export class BrowserRuntime {
|
|
|
413
472
|
public start(): Promise<void> {
|
|
414
473
|
if (this.lifecycleState === 'running') return Promise.resolve();
|
|
415
474
|
if (this.startPromise) return this.startPromise;
|
|
416
|
-
if (this.
|
|
475
|
+
if (this.stopCleanupPromise) return this.stopCleanupPromise.then(() => this.start());
|
|
417
476
|
this.lifecycleState = 'starting';
|
|
418
477
|
const epoch = ++this.lifecycleEpoch;
|
|
419
478
|
this.lifecycleController = new AbortController();
|
|
@@ -435,17 +494,38 @@ export class BrowserRuntime {
|
|
|
435
494
|
|
|
436
495
|
public stop(): Promise<void> {
|
|
437
496
|
if (this.stopPromise) return this.stopPromise;
|
|
438
|
-
if (
|
|
497
|
+
if (
|
|
498
|
+
this.lifecycleState === 'stopped'
|
|
499
|
+
&& !this.lockHandle
|
|
500
|
+
&& !this.startPromise
|
|
501
|
+
&& !this.stopCleanupPromise
|
|
502
|
+
) {
|
|
439
503
|
return Promise.resolve();
|
|
440
504
|
}
|
|
441
|
-
this.
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
505
|
+
if (!this.stopCleanupPromise) {
|
|
506
|
+
this.lifecycleState = 'stopping';
|
|
507
|
+
this.lifecycleEpoch += 1;
|
|
508
|
+
this.lifecycleController.abort(new BrowserRuntimeError('ABORTED'));
|
|
509
|
+
const startup = this.startPromise;
|
|
510
|
+
const cleanup = (async () => {
|
|
511
|
+
await startup?.catch(() => undefined);
|
|
512
|
+
await this.stopInternal();
|
|
513
|
+
this.lifecycleState = 'stopped';
|
|
514
|
+
})();
|
|
515
|
+
this.stopCleanupPromise = cleanup;
|
|
516
|
+
void cleanup.then(
|
|
517
|
+
() => {
|
|
518
|
+
if (this.stopCleanupPromise === cleanup) this.stopCleanupPromise = undefined;
|
|
519
|
+
},
|
|
520
|
+
() => {
|
|
521
|
+
if (this.stopCleanupPromise === cleanup) this.stopCleanupPromise = undefined;
|
|
522
|
+
},
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
const cleanup = this.stopCleanupPromise;
|
|
445
526
|
this.stopPromise = (async () => {
|
|
446
|
-
await
|
|
447
|
-
|
|
448
|
-
this.lifecycleState = 'stopped';
|
|
527
|
+
const result = await waitBounded(cleanup, this.options.cleanupTimeoutMs);
|
|
528
|
+
if (!result.settled) throw new BrowserRuntimeError('TIMEOUT');
|
|
449
529
|
})().finally(() => {
|
|
450
530
|
this.stopPromise = undefined;
|
|
451
531
|
});
|
|
@@ -501,9 +581,12 @@ export class BrowserRuntime {
|
|
|
501
581
|
mutex: new TransitionMutex(),
|
|
502
582
|
arbitrationGeneration: 0,
|
|
503
583
|
incarnationGeneration: 0,
|
|
584
|
+
authorityGeneration: 0,
|
|
504
585
|
permanentlyFenced: false,
|
|
505
586
|
retirementPending: false,
|
|
506
587
|
attachmentFenceCount: 0,
|
|
588
|
+
operationQueue: [],
|
|
589
|
+
operationSchedulerRunning: false,
|
|
507
590
|
lifecycleTail: Promise.resolve(),
|
|
508
591
|
lifecycleOperationCount: 0,
|
|
509
592
|
terminationRequestGeneration: 0,
|
|
@@ -638,16 +721,22 @@ export class BrowserRuntime {
|
|
|
638
721
|
const channelId = requestArg.channelId === undefined
|
|
639
722
|
? undefined
|
|
640
723
|
: this.validateIdentifier(requestArg.channelId, 'channelId');
|
|
724
|
+
const runId = requestArg.runId === undefined
|
|
725
|
+
? undefined
|
|
726
|
+
: this.validateIdentifier(requestArg.runId, 'runId');
|
|
641
727
|
const sessionId = requestArg.sessionId === undefined
|
|
642
728
|
? undefined
|
|
643
729
|
: this.validateQualifiedSessionId(requestArg.sessionId);
|
|
644
730
|
if (
|
|
645
731
|
(role === 'agent' && !sessionId)
|
|
646
732
|
|| (role === 'human' && sessionId !== undefined)
|
|
647
|
-
|| (source === 'flex' && (!scopeId || !channelId ||
|
|
733
|
+
|| (source === 'flex' && (!scopeId || !channelId || !runId
|
|
734
|
+
|| sessionId?.harnessId !== 'flex'))
|
|
648
735
|
|| (source === 'mcp' && (scopeId !== undefined || channelId !== undefined
|
|
736
|
+
|| runId !== undefined
|
|
649
737
|
|| sessionId?.harnessId !== 'opencode'))
|
|
650
|
-
|| (source === 'human' && (scopeId !== undefined || channelId !== undefined
|
|
738
|
+
|| (source === 'human' && (scopeId !== undefined || channelId !== undefined
|
|
739
|
+
|| runId !== undefined))
|
|
651
740
|
) throw new BrowserRuntimeError('INVALID_INPUT');
|
|
652
741
|
const authority = this.validateExpectedCapabilityBinding({
|
|
653
742
|
projectId,
|
|
@@ -660,6 +749,7 @@ export class BrowserRuntime {
|
|
|
660
749
|
source,
|
|
661
750
|
...(scopeId ? { scopeId } : {}),
|
|
662
751
|
...(channelId ? { channelId } : {}),
|
|
752
|
+
...(runId ? { runId } : {}),
|
|
663
753
|
...(sessionId ? { sessionId } : {}),
|
|
664
754
|
} as TBrowserCapabilityAuthorizationRequest);
|
|
665
755
|
this.assertCapabilityBinding(slot, {
|
|
@@ -925,6 +1015,34 @@ export class BrowserRuntime {
|
|
|
925
1015
|
return this.resourceState(session.getState());
|
|
926
1016
|
}
|
|
927
1017
|
|
|
1018
|
+
/** @internal */
|
|
1019
|
+
public getLeaseAuthority(lease: ILeaseRecord): TBrowserRuntimeLeaseAuthority {
|
|
1020
|
+
this.requireRunning();
|
|
1021
|
+
this.requireValidLease(lease);
|
|
1022
|
+
return this.createLeaseAuthority(lease);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/** @internal */
|
|
1026
|
+
public isLeaseAuthorityCurrent(
|
|
1027
|
+
lease: ILeaseRecord,
|
|
1028
|
+
authorityArg: TBrowserRuntimeLeaseAuthority,
|
|
1029
|
+
): boolean {
|
|
1030
|
+
const authority = this.validateLeaseAuthority(authorityArg);
|
|
1031
|
+
try {
|
|
1032
|
+
this.requireRunning();
|
|
1033
|
+
this.requireValidLease(lease);
|
|
1034
|
+
this.assertSlotAvailable(lease.slot);
|
|
1035
|
+
} catch {
|
|
1036
|
+
return false;
|
|
1037
|
+
}
|
|
1038
|
+
return authority.runtimeAuthorityId === this.runtimeAuthorityId
|
|
1039
|
+
&& authority.authorityGeneration === lease.authorityGeneration
|
|
1040
|
+
&& authority.incarnationGeneration === lease.slot.incarnationGeneration
|
|
1041
|
+
&& authority.capabilityId === lease.capability.capabilityId
|
|
1042
|
+
&& authority.leaseId === lease.leaseId
|
|
1043
|
+
&& this.capabilityIdentitiesEqual(authority, this.describeCapabilityIdentity(lease.capability));
|
|
1044
|
+
}
|
|
1045
|
+
|
|
928
1046
|
/** @internal */
|
|
929
1047
|
public async executeLeaseAgentAction(
|
|
930
1048
|
lease: ILeaseRecord,
|
|
@@ -933,9 +1051,13 @@ export class BrowserRuntime {
|
|
|
933
1051
|
): Promise<TBrowserAgentActionResult> {
|
|
934
1052
|
if (lease.role !== 'agent') throw new BrowserRuntimeError('CAPABILITY_INVALID');
|
|
935
1053
|
const action = validateAgentAction(actionArg);
|
|
936
|
-
return this.runOperation(
|
|
937
|
-
|
|
938
|
-
|
|
1054
|
+
return this.runOperation(
|
|
1055
|
+
lease,
|
|
1056
|
+
action.action,
|
|
1057
|
+
this.classifyAgentAction(action),
|
|
1058
|
+
operationOptions,
|
|
1059
|
+
async (signal, session) => this.executeAgentActionInternal(lease, session, action, signal),
|
|
1060
|
+
);
|
|
939
1061
|
}
|
|
940
1062
|
|
|
941
1063
|
/** @internal */
|
|
@@ -945,7 +1067,7 @@ export class BrowserRuntime {
|
|
|
945
1067
|
operationOptions: IBrowserRuntimeOperationOptions = {},
|
|
946
1068
|
): Promise<IBrowserRuntimeState> {
|
|
947
1069
|
this.requireHuman(lease);
|
|
948
|
-
return this.runOperation(lease, 'createTab', operationOptions, async (signal, session) => {
|
|
1070
|
+
return this.runOperation(lease, 'createTab', 'tab', operationOptions, async (signal, session) => {
|
|
949
1071
|
if (session.getState().tabs.length >= this.options.maxTabsPerResource) {
|
|
950
1072
|
throw new BrowserRuntimeError('QUOTA_EXCEEDED');
|
|
951
1073
|
}
|
|
@@ -962,7 +1084,7 @@ export class BrowserRuntime {
|
|
|
962
1084
|
): Promise<IBrowserRuntimeState> {
|
|
963
1085
|
this.requireHuman(lease);
|
|
964
1086
|
const tabId = validateBoundedString(tabIdArg, 'tabId', 1, 128);
|
|
965
|
-
return this.runOperation(lease, 'activateTab', operationOptions, async (signal, session) => {
|
|
1087
|
+
return this.runOperation(lease, 'activateTab', 'tab', operationOptions, async (signal, session) => {
|
|
966
1088
|
await session.activateTab(tabId, { signal });
|
|
967
1089
|
return this.resourceState(session.getState());
|
|
968
1090
|
});
|
|
@@ -976,7 +1098,7 @@ export class BrowserRuntime {
|
|
|
976
1098
|
): Promise<IBrowserRuntimeState> {
|
|
977
1099
|
this.requireHuman(lease);
|
|
978
1100
|
const tabId = validateBoundedString(tabIdArg, 'tabId', 1, 128);
|
|
979
|
-
return this.runOperation(lease, 'closeTab', operationOptions, async (signal, session) => {
|
|
1101
|
+
return this.runOperation(lease, 'closeTab', 'tab', operationOptions, async (signal, session) => {
|
|
980
1102
|
await session.closeTab(tabId, { signal });
|
|
981
1103
|
return this.resourceState(session.getState());
|
|
982
1104
|
});
|
|
@@ -990,7 +1112,7 @@ export class BrowserRuntime {
|
|
|
990
1112
|
operationOptions: IBrowserRuntimeOperationOptions = {},
|
|
991
1113
|
): Promise<IBrowserRuntimeState> {
|
|
992
1114
|
this.requireHuman(lease);
|
|
993
|
-
return this.runOperation(lease, action, operationOptions, async (signal, session) => {
|
|
1115
|
+
return this.runOperation(lease, action, 'navigation', operationOptions, async (signal, session) => {
|
|
994
1116
|
await session[action](options, { signal });
|
|
995
1117
|
return this.resourceState(session.getState());
|
|
996
1118
|
});
|
|
@@ -1004,9 +1126,13 @@ export class BrowserRuntime {
|
|
|
1004
1126
|
): Promise<TBrowserAgentActionResult> {
|
|
1005
1127
|
this.requireHuman(lease);
|
|
1006
1128
|
const action = validateAgentAction(actionArg);
|
|
1007
|
-
return this.runOperation(
|
|
1008
|
-
|
|
1009
|
-
|
|
1129
|
+
return this.runOperation(
|
|
1130
|
+
lease,
|
|
1131
|
+
action.action,
|
|
1132
|
+
this.classifyAgentAction(action),
|
|
1133
|
+
operationOptions,
|
|
1134
|
+
async (signal, session) => this.executeAgentActionInternal(lease, session, action, signal),
|
|
1135
|
+
);
|
|
1010
1136
|
}
|
|
1011
1137
|
|
|
1012
1138
|
/** @internal */
|
|
@@ -1016,9 +1142,15 @@ export class BrowserRuntime {
|
|
|
1016
1142
|
operationOptions: IBrowserRuntimeOperationOptions = {},
|
|
1017
1143
|
): Promise<void> {
|
|
1018
1144
|
this.requireHuman(lease);
|
|
1019
|
-
await this.runOperation(
|
|
1145
|
+
await this.runOperation(
|
|
1146
|
+
lease,
|
|
1147
|
+
'setViewport',
|
|
1148
|
+
'viewport',
|
|
1149
|
+
operationOptions,
|
|
1150
|
+
async (signal, session) => {
|
|
1020
1151
|
await session.setViewport(viewport, { signal });
|
|
1021
|
-
|
|
1152
|
+
},
|
|
1153
|
+
);
|
|
1022
1154
|
}
|
|
1023
1155
|
|
|
1024
1156
|
/** @internal */
|
|
@@ -1033,7 +1165,7 @@ export class BrowserRuntime {
|
|
|
1033
1165
|
operationOptions: IBrowserRuntimeOperationOptions = {},
|
|
1034
1166
|
): Promise<void> {
|
|
1035
1167
|
this.requireHuman(lease);
|
|
1036
|
-
await this.runOperation(lease, action, operationOptions, async (_signal, session) => {
|
|
1168
|
+
await this.runOperation(lease, action, 'raw-input', operationOptions, async (_signal, session) => {
|
|
1037
1169
|
if (action === 'dispatchMouse') {
|
|
1038
1170
|
await session.dispatchMouse(input as plugins.smartpuppeteer.ILiveBrowserMouseInput);
|
|
1039
1171
|
} else if (action === 'dispatchWheel') {
|
|
@@ -1059,7 +1191,13 @@ export class BrowserRuntime {
|
|
|
1059
1191
|
try {
|
|
1060
1192
|
this.requireValidLease(lease);
|
|
1061
1193
|
if (slot.frameSubscription) throw new BrowserRuntimeError('BUSY');
|
|
1062
|
-
subscription = {
|
|
1194
|
+
subscription = {
|
|
1195
|
+
lease,
|
|
1196
|
+
listener,
|
|
1197
|
+
outstanding: new Map(),
|
|
1198
|
+
highestSequence: 0,
|
|
1199
|
+
closed: false,
|
|
1200
|
+
};
|
|
1063
1201
|
slot.frameSubscription = subscription;
|
|
1064
1202
|
this.pushToSubscription(subscription, {
|
|
1065
1203
|
type: 'state',
|
|
@@ -1076,46 +1214,28 @@ export class BrowserRuntime {
|
|
|
1076
1214
|
/** @internal */
|
|
1077
1215
|
public async acknowledgeLeaseFrame(
|
|
1078
1216
|
lease: ILeaseRecord,
|
|
1079
|
-
|
|
1217
|
+
acknowledgementArg: plugins.smartpuppeteer.ILiveBrowserFrameAcknowledgementRequest,
|
|
1080
1218
|
): Promise<boolean> {
|
|
1081
1219
|
this.requireHuman(lease);
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
}
|
|
1085
|
-
validateBoundedString(acknowledgement.tabId, 'tabId', 1, 128);
|
|
1086
|
-
validateInteger(acknowledgement.sequence, 'sequence', 0, Number.MAX_SAFE_INTEGER);
|
|
1087
|
-
validateInteger(acknowledgement.generation, 'generation', 0, Number.MAX_SAFE_INTEGER);
|
|
1088
|
-
validateInteger(
|
|
1089
|
-
acknowledgement.viewportRevision,
|
|
1090
|
-
'viewportRevision',
|
|
1091
|
-
0,
|
|
1092
|
-
Number.MAX_SAFE_INTEGER,
|
|
1093
|
-
);
|
|
1220
|
+
const acknowledgement = this.validateFrameAcknowledgement(acknowledgementArg, true);
|
|
1221
|
+
const key = this.frameAcknowledgementKey(acknowledgement);
|
|
1094
1222
|
const slot = lease.slot;
|
|
1095
1223
|
const subscription = slot.frameSubscription;
|
|
1096
1224
|
this.requireValidLease(lease);
|
|
1097
|
-
if (!subscription || subscription.lease !== lease
|
|
1098
|
-
const
|
|
1099
|
-
if (
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
.catch(() => ({ settled: true as const, value: { accepted: false } }));
|
|
1112
|
-
if (!result.settled || !result.value.accepted) {
|
|
1113
|
-
operation.catch(() => undefined);
|
|
1114
|
-
await this.options.beforeFrameFailureTermination?.();
|
|
1115
|
-
await this.handleFrameFailure(slot, session, incarnationGeneration, lease);
|
|
1116
|
-
return false;
|
|
1117
|
-
}
|
|
1118
|
-
return true;
|
|
1225
|
+
if (!subscription || subscription.lease !== lease) return false;
|
|
1226
|
+
const outstanding = subscription.outstanding.get(key);
|
|
1227
|
+
if (!outstanding) return false;
|
|
1228
|
+
subscription.outstanding.delete(key);
|
|
1229
|
+
clearTimeout(outstanding.timer);
|
|
1230
|
+
const { session, incarnationGeneration } = outstanding;
|
|
1231
|
+
const outcome = await this.waitForFrameAcknowledgement(
|
|
1232
|
+
session,
|
|
1233
|
+
outstanding.acknowledgement,
|
|
1234
|
+
);
|
|
1235
|
+
if (outcome.status === 'fulfilled') return outcome.accepted;
|
|
1236
|
+
await this.options.beforeFrameFailureTermination?.();
|
|
1237
|
+
await this.handleFrameFailure(slot, session, incarnationGeneration, subscription.lease);
|
|
1238
|
+
return false;
|
|
1119
1239
|
}
|
|
1120
1240
|
|
|
1121
1241
|
/** @internal */
|
|
@@ -1229,7 +1349,9 @@ export class BrowserRuntime {
|
|
|
1229
1349
|
}
|
|
1230
1350
|
|
|
1231
1351
|
private requireRunning(): void {
|
|
1232
|
-
if (this.lifecycleState !== 'running'
|
|
1352
|
+
if (this.lifecycleState !== 'running' || this.lifecycleController.signal.aborted) {
|
|
1353
|
+
throw new BrowserRuntimeError('NOT_RUNNING');
|
|
1354
|
+
}
|
|
1233
1355
|
}
|
|
1234
1356
|
|
|
1235
1357
|
private async acquireAgentLease(
|
|
@@ -1347,6 +1469,11 @@ export class BrowserRuntime {
|
|
|
1347
1469
|
clearTimeout(slot.idleTimer);
|
|
1348
1470
|
slot.idleTimer = undefined;
|
|
1349
1471
|
}
|
|
1472
|
+
if (slot.authorityGeneration >= Number.MAX_SAFE_INTEGER) {
|
|
1473
|
+
slot.permanentlyFenced = true;
|
|
1474
|
+
throw new BrowserRuntimeError('FENCED');
|
|
1475
|
+
}
|
|
1476
|
+
slot.authorityGeneration += 1;
|
|
1350
1477
|
const lease: ILeaseRecord = {
|
|
1351
1478
|
leaseId: randomId(18),
|
|
1352
1479
|
capability,
|
|
@@ -1355,6 +1482,7 @@ export class BrowserRuntime {
|
|
|
1355
1482
|
role: capability.role,
|
|
1356
1483
|
slot,
|
|
1357
1484
|
controller: new AbortController(),
|
|
1485
|
+
authorityGeneration: slot.authorityGeneration,
|
|
1358
1486
|
released: false,
|
|
1359
1487
|
};
|
|
1360
1488
|
slot.lease = lease;
|
|
@@ -1387,6 +1515,9 @@ export class BrowserRuntime {
|
|
|
1387
1515
|
forceNoSandbox: false,
|
|
1388
1516
|
usePipe: true,
|
|
1389
1517
|
allowEvaluation: true,
|
|
1518
|
+
screencast: {
|
|
1519
|
+
maxOutstandingFrames: this.options.maxOutstandingFrames,
|
|
1520
|
+
},
|
|
1390
1521
|
launchOptions: {
|
|
1391
1522
|
headless: true,
|
|
1392
1523
|
userDataDir: profileDirectory,
|
|
@@ -1503,9 +1634,10 @@ export class BrowserRuntime {
|
|
|
1503
1634
|
}
|
|
1504
1635
|
}
|
|
1505
1636
|
|
|
1506
|
-
private
|
|
1637
|
+
private runOperation<T>(
|
|
1507
1638
|
lease: ILeaseRecord,
|
|
1508
1639
|
action: string,
|
|
1640
|
+
classification: TBrowserRuntimeOperationClassification,
|
|
1509
1641
|
operationOptions: IBrowserRuntimeOperationOptions,
|
|
1510
1642
|
execute: (signal: AbortSignal, session: ILiveBrowserSessionLike) => Promise<T>,
|
|
1511
1643
|
): Promise<T> {
|
|
@@ -1522,10 +1654,94 @@ export class BrowserRuntime {
|
|
|
1522
1654
|
) throw new BrowserRuntimeError('INVALID_INPUT');
|
|
1523
1655
|
if (operationOptions.signal?.aborted) throw new BrowserRuntimeError('ABORTED');
|
|
1524
1656
|
const slot = lease.slot;
|
|
1525
|
-
|
|
1526
|
-
|
|
1657
|
+
this.requireValidLease(lease);
|
|
1658
|
+
this.assertSlotAvailable(slot);
|
|
1659
|
+
if (slot.operationQueue.length >= this.options.maxQueuedOperationsPerLease) {
|
|
1660
|
+
throw new BrowserRuntimeError('QUOTA_EXCEEDED');
|
|
1661
|
+
}
|
|
1662
|
+
const timeoutMs = operationOptions.timeoutMs === undefined
|
|
1663
|
+
? this.options.operationTimeoutMs
|
|
1664
|
+
: validateInteger(operationOptions.timeoutMs, 'timeoutMs', 100, 120_000);
|
|
1665
|
+
const signal = AbortSignal.any([
|
|
1666
|
+
lease.controller.signal,
|
|
1667
|
+
this.lifecycleController.signal,
|
|
1668
|
+
operationOptions.signal ?? new AbortController().signal,
|
|
1669
|
+
]);
|
|
1670
|
+
return new Promise<T>((resolve, reject) => {
|
|
1671
|
+
const queued: IQueuedOperationRecord = {
|
|
1672
|
+
operationId: randomId(18),
|
|
1673
|
+
lease,
|
|
1674
|
+
action,
|
|
1675
|
+
classification,
|
|
1676
|
+
timeoutMs,
|
|
1677
|
+
externalSignal: operationOptions.signal,
|
|
1678
|
+
onOperationStarted: operationOptions.onOperationStarted,
|
|
1679
|
+
signal,
|
|
1680
|
+
execute: execute as IQueuedOperationRecord['execute'],
|
|
1681
|
+
resolve: (value) => resolve(value as T),
|
|
1682
|
+
reject,
|
|
1683
|
+
state: 'queued',
|
|
1684
|
+
};
|
|
1685
|
+
const onQueuedAbort = (): void => {
|
|
1686
|
+
if (queued.state !== 'queued' && queued.state !== 'starting') return;
|
|
1687
|
+
if (queued.state === 'queued') {
|
|
1688
|
+
const index = slot.operationQueue.indexOf(queued);
|
|
1689
|
+
if (index >= 0) slot.operationQueue.splice(index, 1);
|
|
1690
|
+
}
|
|
1691
|
+
queued.state = 'settled';
|
|
1692
|
+
signal.removeEventListener('abort', onQueuedAbort);
|
|
1693
|
+
reject(this.queuedOperationAbortError(queued));
|
|
1694
|
+
};
|
|
1695
|
+
queued.onQueuedAbort = onQueuedAbort;
|
|
1696
|
+
signal.addEventListener('abort', onQueuedAbort, { once: true });
|
|
1697
|
+
if (signal.aborted) {
|
|
1698
|
+
onQueuedAbort();
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
slot.operationQueue.push(queued);
|
|
1702
|
+
this.drainOperationQueue(slot);
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
private drainOperationQueue(slot: IResourceSlot): void {
|
|
1707
|
+
if (slot.operationSchedulerRunning) return;
|
|
1708
|
+
slot.operationSchedulerRunning = true;
|
|
1709
|
+
const scheduler = (async () => {
|
|
1710
|
+
while (slot.operationQueue.length > 0) {
|
|
1711
|
+
const queued = slot.operationQueue.shift()!;
|
|
1712
|
+
if (queued.state === 'settled') continue;
|
|
1713
|
+
queued.state = 'starting';
|
|
1714
|
+
try {
|
|
1715
|
+
const result = await this.executeQueuedOperation(queued);
|
|
1716
|
+
if ((queued as IQueuedOperationRecord).state !== 'settled') {
|
|
1717
|
+
queued.state = 'settled';
|
|
1718
|
+
queued.resolve(result);
|
|
1719
|
+
}
|
|
1720
|
+
} catch (error) {
|
|
1721
|
+
if (queued.state !== 'settled') {
|
|
1722
|
+
queued.state = 'settled';
|
|
1723
|
+
queued.reject(error);
|
|
1724
|
+
}
|
|
1725
|
+
} finally {
|
|
1726
|
+
if (queued.onQueuedAbort) {
|
|
1727
|
+
queued.signal.removeEventListener('abort', queued.onQueuedAbort);
|
|
1728
|
+
queued.onQueuedAbort = undefined;
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
})().finally(() => {
|
|
1733
|
+
slot.operationSchedulerRunning = false;
|
|
1734
|
+
if (slot.operationQueue.length > 0) this.drainOperationQueue(slot);
|
|
1735
|
+
});
|
|
1736
|
+
void scheduler.catch(() => undefined);
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
private async executeQueuedOperation(queued: IQueuedOperationRecord): Promise<unknown> {
|
|
1740
|
+
const { lease, action, classification } = queued;
|
|
1741
|
+
const slot = lease.slot;
|
|
1742
|
+
const release = await slot.mutex.acquire();
|
|
1527
1743
|
let operation: IOperationRecord;
|
|
1528
|
-
let executionPromise: Promise<
|
|
1744
|
+
let executionPromise: Promise<unknown> | undefined;
|
|
1529
1745
|
let combinedSignal: AbortSignal;
|
|
1530
1746
|
let timeout: ReturnType<typeof setTimeout>;
|
|
1531
1747
|
let session: ILiveBrowserSessionLike;
|
|
@@ -1534,35 +1750,34 @@ export class BrowserRuntime {
|
|
|
1534
1750
|
let externalAbort = false;
|
|
1535
1751
|
let onExternalAbort: (() => void) | undefined;
|
|
1536
1752
|
let resolvePreflight!: () => void;
|
|
1537
|
-
|
|
1753
|
+
let startedAt: number;
|
|
1538
1754
|
try {
|
|
1755
|
+
if (queued.state === 'settled') return undefined;
|
|
1756
|
+
queued.signal.throwIfAborted();
|
|
1539
1757
|
this.requireValidLease(lease);
|
|
1540
1758
|
this.assertSlotAvailable(slot);
|
|
1541
1759
|
if (slot.operation) throw new BrowserRuntimeError('BUSY');
|
|
1542
1760
|
session = slot.session!;
|
|
1543
1761
|
generation = slot.arbitrationGeneration;
|
|
1544
1762
|
incarnationGeneration = slot.incarnationGeneration;
|
|
1545
|
-
|
|
1546
|
-
? this.options.operationTimeoutMs
|
|
1547
|
-
: validateInteger(operationOptions.timeoutMs, 'timeoutMs', 100, 120_000);
|
|
1763
|
+
startedAt = Date.now();
|
|
1548
1764
|
const controller = new AbortController();
|
|
1549
1765
|
const timeoutController = new AbortController();
|
|
1550
1766
|
timeout = setTimeout(() => {
|
|
1551
1767
|
timeoutController.abort(new BrowserRuntimeError('TIMEOUT'));
|
|
1552
|
-
}, timeoutMs);
|
|
1768
|
+
}, queued.timeoutMs);
|
|
1553
1769
|
timeout.unref();
|
|
1554
|
-
const externalSignal =
|
|
1770
|
+
const externalSignal = queued.externalSignal;
|
|
1555
1771
|
onExternalAbort = () => { externalAbort = true; };
|
|
1556
1772
|
externalSignal?.addEventListener('abort', onExternalAbort, { once: true });
|
|
1557
1773
|
if (externalSignal?.aborted) externalAbort = true;
|
|
1558
1774
|
combinedSignal = AbortSignal.any([
|
|
1559
1775
|
controller.signal,
|
|
1560
|
-
|
|
1561
|
-
externalSignal ?? new AbortController().signal,
|
|
1776
|
+
queued.signal,
|
|
1562
1777
|
timeoutController.signal,
|
|
1563
1778
|
]);
|
|
1564
1779
|
operation = {
|
|
1565
|
-
operationId:
|
|
1780
|
+
operationId: queued.operationId,
|
|
1566
1781
|
lease,
|
|
1567
1782
|
controller,
|
|
1568
1783
|
action,
|
|
@@ -1572,8 +1787,13 @@ export class BrowserRuntime {
|
|
|
1572
1787
|
promise: new Promise<void>((resolve) => { resolvePreflight = resolve; }),
|
|
1573
1788
|
};
|
|
1574
1789
|
slot.operation = operation;
|
|
1790
|
+
queued.state = 'active';
|
|
1791
|
+
if (queued.onQueuedAbort) {
|
|
1792
|
+
queued.signal.removeEventListener('abort', queued.onQueuedAbort);
|
|
1793
|
+
queued.onQueuedAbort = undefined;
|
|
1794
|
+
}
|
|
1575
1795
|
try {
|
|
1576
|
-
|
|
1796
|
+
queued.onOperationStarted?.(operation.operationId);
|
|
1577
1797
|
} catch {
|
|
1578
1798
|
controller.abort(new BrowserRuntimeError('ABORTED'));
|
|
1579
1799
|
}
|
|
@@ -1581,9 +1801,15 @@ export class BrowserRuntime {
|
|
|
1581
1801
|
release();
|
|
1582
1802
|
}
|
|
1583
1803
|
|
|
1584
|
-
const operationIdentity = this.createOperationIdentity(
|
|
1804
|
+
const operationIdentity = this.createOperationIdentity(
|
|
1805
|
+
lease,
|
|
1806
|
+
operation,
|
|
1807
|
+
action,
|
|
1808
|
+
classification,
|
|
1809
|
+
startedAt,
|
|
1810
|
+
);
|
|
1585
1811
|
|
|
1586
|
-
let result:
|
|
1812
|
+
let result: unknown;
|
|
1587
1813
|
let failed = false;
|
|
1588
1814
|
let failure: unknown;
|
|
1589
1815
|
let executionStarted = false;
|
|
@@ -1602,7 +1828,7 @@ export class BrowserRuntime {
|
|
|
1602
1828
|
|| slot.incarnationGeneration !== incarnationGeneration
|
|
1603
1829
|
) throw new BrowserRuntimeError('ABORTED');
|
|
1604
1830
|
executionStarted = true;
|
|
1605
|
-
executionPromise = execute(combinedSignal, session);
|
|
1831
|
+
executionPromise = queued.execute(combinedSignal, session);
|
|
1606
1832
|
operation.promise = executionPromise.then(() => undefined, () => undefined);
|
|
1607
1833
|
} finally {
|
|
1608
1834
|
executionRelease();
|
|
@@ -1628,7 +1854,7 @@ export class BrowserRuntime {
|
|
|
1628
1854
|
failure = this.normalizeOperationError(error, combinedSignal!, externalAbort);
|
|
1629
1855
|
} finally {
|
|
1630
1856
|
clearTimeout(timeout!);
|
|
1631
|
-
|
|
1857
|
+
queued.externalSignal?.removeEventListener('abort', onExternalAbort!);
|
|
1632
1858
|
resolvePreflight();
|
|
1633
1859
|
const cleanupSlot = async (): Promise<void> => {
|
|
1634
1860
|
const cleanupRelease = await slot.mutex.acquire();
|
|
@@ -1658,7 +1884,14 @@ export class BrowserRuntime {
|
|
|
1658
1884
|
}));
|
|
1659
1885
|
}
|
|
1660
1886
|
if (failed) throw failure;
|
|
1661
|
-
return result
|
|
1887
|
+
return result;
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
private queuedOperationAbortError(queued: IQueuedOperationRecord): BrowserRuntimeError {
|
|
1891
|
+
if (queued.externalSignal?.aborted) return new BrowserRuntimeError('ABORTED');
|
|
1892
|
+
return queued.signal.reason instanceof BrowserRuntimeError
|
|
1893
|
+
? queued.signal.reason
|
|
1894
|
+
: new BrowserRuntimeError('ABORTED');
|
|
1662
1895
|
}
|
|
1663
1896
|
|
|
1664
1897
|
private normalizeOperationError(
|
|
@@ -1733,6 +1966,7 @@ export class BrowserRuntime {
|
|
|
1733
1966
|
leaseArg: ILeaseRecord,
|
|
1734
1967
|
operationArg: IOperationRecord,
|
|
1735
1968
|
actionArg: string,
|
|
1969
|
+
classificationArg: TBrowserRuntimeOperationClassification,
|
|
1736
1970
|
startedAtArg: number,
|
|
1737
1971
|
): TBrowserRuntimeOperationIdentity {
|
|
1738
1972
|
const identity = this.describeCapabilityIdentity(leaseArg.capability);
|
|
@@ -1746,10 +1980,90 @@ export class BrowserRuntime {
|
|
|
1746
1980
|
capabilityId: leaseArg.capability.capabilityId,
|
|
1747
1981
|
leaseId: leaseArg.leaseId,
|
|
1748
1982
|
action: actionArg,
|
|
1983
|
+
classification: classificationArg,
|
|
1749
1984
|
startedAt: startedAtArg,
|
|
1750
1985
|
}) as TBrowserRuntimeOperationIdentity;
|
|
1751
1986
|
}
|
|
1752
1987
|
|
|
1988
|
+
private createLeaseAuthority(lease: ILeaseRecord): TBrowserRuntimeLeaseAuthority {
|
|
1989
|
+
const identity = this.describeCapabilityIdentity(lease.capability);
|
|
1990
|
+
const sessionId = identity.role === 'agent'
|
|
1991
|
+
? Object.freeze({ ...identity.sessionId })
|
|
1992
|
+
: undefined;
|
|
1993
|
+
return Object.freeze({
|
|
1994
|
+
...identity,
|
|
1995
|
+
...(sessionId ? { sessionId } : {}),
|
|
1996
|
+
runtimeAuthorityId: this.runtimeAuthorityId,
|
|
1997
|
+
authorityGeneration: lease.authorityGeneration,
|
|
1998
|
+
incarnationGeneration: lease.slot.incarnationGeneration,
|
|
1999
|
+
capabilityId: lease.capability.capabilityId,
|
|
2000
|
+
leaseId: lease.leaseId,
|
|
2001
|
+
}) as TBrowserRuntimeLeaseAuthority;
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
private validateLeaseAuthority(value: unknown): TBrowserRuntimeLeaseAuthority {
|
|
2005
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
2006
|
+
throw new BrowserRuntimeError('INVALID_INPUT');
|
|
2007
|
+
}
|
|
2008
|
+
const candidate = value as Record<string, unknown>;
|
|
2009
|
+
const isFlex = candidate.role === 'agent' && candidate.source === 'flex';
|
|
2010
|
+
const isMcp = candidate.role === 'agent' && candidate.source === 'mcp';
|
|
2011
|
+
const isHuman = candidate.role === 'human' && candidate.source === 'human';
|
|
2012
|
+
if (!isFlex && !isMcp && !isHuman) throw new BrowserRuntimeError('INVALID_INPUT');
|
|
2013
|
+
const record = validateExactKeys(value, [
|
|
2014
|
+
'projectId',
|
|
2015
|
+
'browserResourceId',
|
|
2016
|
+
'attachmentAuthorityId',
|
|
2017
|
+
'attachmentRevision',
|
|
2018
|
+
'actorId',
|
|
2019
|
+
'peerId',
|
|
2020
|
+
'role',
|
|
2021
|
+
'source',
|
|
2022
|
+
'runtimeAuthorityId',
|
|
2023
|
+
'authorityGeneration',
|
|
2024
|
+
'incarnationGeneration',
|
|
2025
|
+
'capabilityId',
|
|
2026
|
+
'leaseId',
|
|
2027
|
+
...(isFlex ? ['sessionId', 'scopeId', 'channelId', 'runId'] : []),
|
|
2028
|
+
...(isMcp ? ['sessionId'] : []),
|
|
2029
|
+
], 'lease authority');
|
|
2030
|
+
const identity = this.validateExpectedCapabilityBinding(
|
|
2031
|
+
record as unknown as TBrowserCapabilityAuthorizationRequest,
|
|
2032
|
+
);
|
|
2033
|
+
const runtimeAuthorityId = validateBoundedString(
|
|
2034
|
+
record.runtimeAuthorityId,
|
|
2035
|
+
'runtimeAuthorityId',
|
|
2036
|
+
16,
|
|
2037
|
+
128,
|
|
2038
|
+
);
|
|
2039
|
+
const capabilityId = validateBoundedString(record.capabilityId, 'capabilityId', 1, 128);
|
|
2040
|
+
const leaseId = validateBoundedString(record.leaseId, 'leaseId', 1, 128);
|
|
2041
|
+
return {
|
|
2042
|
+
...identity,
|
|
2043
|
+
runtimeAuthorityId,
|
|
2044
|
+
authorityGeneration: validateInteger(
|
|
2045
|
+
record.authorityGeneration,
|
|
2046
|
+
'authorityGeneration',
|
|
2047
|
+
1,
|
|
2048
|
+
Number.MAX_SAFE_INTEGER,
|
|
2049
|
+
),
|
|
2050
|
+
incarnationGeneration: validateInteger(
|
|
2051
|
+
record.incarnationGeneration,
|
|
2052
|
+
'incarnationGeneration',
|
|
2053
|
+
1,
|
|
2054
|
+
Number.MAX_SAFE_INTEGER,
|
|
2055
|
+
),
|
|
2056
|
+
capabilityId,
|
|
2057
|
+
leaseId,
|
|
2058
|
+
} as TBrowserRuntimeLeaseAuthority;
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
private classifyAgentAction(
|
|
2062
|
+
action: TBrowserAgentAction,
|
|
2063
|
+
): TBrowserRuntimeOperationClassification {
|
|
2064
|
+
return action.action === 'navigate' ? 'navigation' : 'agent-action';
|
|
2065
|
+
}
|
|
2066
|
+
|
|
1753
2067
|
private async executeAgentActionInternal(
|
|
1754
2068
|
lease: ILeaseRecord,
|
|
1755
2069
|
session: ILiveBrowserSessionLike,
|
|
@@ -1897,12 +2211,22 @@ export class BrowserRuntime {
|
|
|
1897
2211
|
}
|
|
1898
2212
|
const subscription = slot.frameSubscription;
|
|
1899
2213
|
if (event.type === 'frame') {
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
2214
|
+
let acknowledgement: plugins.smartpuppeteer.ILiveBrowserFrameAcknowledgementRequest;
|
|
2215
|
+
try {
|
|
2216
|
+
if (!(event.frame.data instanceof Uint8Array)) {
|
|
2217
|
+
throw new BrowserRuntimeError('INVALID_INPUT');
|
|
2218
|
+
}
|
|
2219
|
+
acknowledgement = this.validateFrameAcknowledgement(event.frame);
|
|
2220
|
+
} catch {
|
|
2221
|
+
const failedLease = slot.session === session ? (slot.lease ?? null) : null;
|
|
2222
|
+
this.trackCleanup(this.handleFrameFailure(
|
|
2223
|
+
slot,
|
|
2224
|
+
session,
|
|
2225
|
+
slot.incarnationGeneration,
|
|
2226
|
+
failedLease,
|
|
2227
|
+
));
|
|
2228
|
+
return;
|
|
2229
|
+
}
|
|
1906
2230
|
if (!subscription || subscription.closed) {
|
|
1907
2231
|
this.trackCleanup(this.acknowledgeFrameInBackground(slot, session, acknowledgement));
|
|
1908
2232
|
return;
|
|
@@ -1914,10 +2238,17 @@ export class BrowserRuntime {
|
|
|
1914
2238
|
this.trackCleanup(this.revokeCapabilityRecord(subscription.lease.capability));
|
|
1915
2239
|
return;
|
|
1916
2240
|
}
|
|
1917
|
-
|
|
1918
|
-
|
|
2241
|
+
const key = this.frameAcknowledgementKey(acknowledgement);
|
|
2242
|
+
if (acknowledgement.sequence <= subscription.highestSequence) {
|
|
2243
|
+
this.trackCleanup(this.handleFrameFailure(
|
|
2244
|
+
slot,
|
|
2245
|
+
session,
|
|
2246
|
+
slot.incarnationGeneration,
|
|
2247
|
+
subscription.lease,
|
|
2248
|
+
));
|
|
1919
2249
|
return;
|
|
1920
2250
|
}
|
|
2251
|
+
subscription.highestSequence = acknowledgement.sequence;
|
|
1921
2252
|
if (event.frame.data.byteLength > this.options.maxFrameBytes) {
|
|
1922
2253
|
this.trackCleanup(this.acknowledgeFrameInBackground(
|
|
1923
2254
|
slot,
|
|
@@ -1931,18 +2262,23 @@ export class BrowserRuntime {
|
|
|
1931
2262
|
});
|
|
1932
2263
|
return;
|
|
1933
2264
|
}
|
|
2265
|
+
while (subscription.outstanding.size >= this.options.maxOutstandingFrames) {
|
|
2266
|
+
const oldestKey = subscription.outstanding.keys().next().value as string | undefined;
|
|
2267
|
+
if (!oldestKey) break;
|
|
2268
|
+
this.trackCleanup(
|
|
2269
|
+
this.retireOutstandingFrame(subscription, oldestKey, false).then(() => undefined),
|
|
2270
|
+
);
|
|
2271
|
+
}
|
|
1934
2272
|
const timer = setTimeout(() => {
|
|
1935
|
-
|
|
1936
|
-
subscription.outstanding = undefined;
|
|
1937
|
-
this.trackCleanup(this.acknowledgeFrameInBackground(
|
|
1938
|
-
slot,
|
|
1939
|
-
session,
|
|
1940
|
-
acknowledgement,
|
|
1941
|
-
true,
|
|
1942
|
-
));
|
|
2273
|
+
this.trackCleanup(this.retireOutstandingFrame(subscription, key, true).then(() => undefined));
|
|
1943
2274
|
}, this.options.frameAcknowledgementTimeoutMs);
|
|
1944
2275
|
timer.unref();
|
|
1945
|
-
subscription.outstanding
|
|
2276
|
+
subscription.outstanding.set(key, {
|
|
2277
|
+
acknowledgement,
|
|
2278
|
+
timer,
|
|
2279
|
+
session,
|
|
2280
|
+
incarnationGeneration: slot.incarnationGeneration,
|
|
2281
|
+
});
|
|
1946
2282
|
this.pushToSubscription(subscription, { type: 'frame', frame: event.frame });
|
|
1947
2283
|
return;
|
|
1948
2284
|
}
|
|
@@ -1975,16 +2311,9 @@ export class BrowserRuntime {
|
|
|
1975
2311
|
const slot = subscription.lease.slot;
|
|
1976
2312
|
const session = slot.session;
|
|
1977
2313
|
const incarnationGeneration = slot.incarnationGeneration;
|
|
1978
|
-
if (event.type === 'frame'
|
|
1979
|
-
const
|
|
1980
|
-
|
|
1981
|
-
subscription.outstanding = undefined;
|
|
1982
|
-
this.trackCleanup(this.acknowledgeFrameInBackground(
|
|
1983
|
-
slot,
|
|
1984
|
-
session!,
|
|
1985
|
-
outstanding.acknowledgement,
|
|
1986
|
-
true,
|
|
1987
|
-
));
|
|
2314
|
+
if (event.type === 'frame') {
|
|
2315
|
+
const key = this.frameAcknowledgementKey(this.validateFrameAcknowledgement(event.frame));
|
|
2316
|
+
this.trackCleanup(this.retireOutstandingFrame(subscription, key, true).then(() => undefined));
|
|
1988
2317
|
return;
|
|
1989
2318
|
}
|
|
1990
2319
|
if (!session) {
|
|
@@ -2008,27 +2337,91 @@ export class BrowserRuntime {
|
|
|
2008
2337
|
subscription.closed = true;
|
|
2009
2338
|
const slot = subscription.lease.slot;
|
|
2010
2339
|
if (slot.frameSubscription === subscription) slot.frameSubscription = undefined;
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2340
|
+
for (const key of [...subscription.outstanding.keys()]) {
|
|
2341
|
+
await this.retireOutstandingFrame(subscription, key, false);
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
private async retireOutstandingFrame(
|
|
2346
|
+
subscription: IFrameSubscriptionRecord,
|
|
2347
|
+
key: string,
|
|
2348
|
+
failLease: boolean,
|
|
2349
|
+
): Promise<boolean> {
|
|
2350
|
+
const outstanding = subscription.outstanding.get(key);
|
|
2351
|
+
if (!outstanding) return false;
|
|
2352
|
+
subscription.outstanding.delete(key);
|
|
2353
|
+
clearTimeout(outstanding.timer);
|
|
2354
|
+
const outcome = await this.waitForFrameAcknowledgement(
|
|
2355
|
+
outstanding.session,
|
|
2356
|
+
outstanding.acknowledgement,
|
|
2357
|
+
);
|
|
2358
|
+
if (outcome.status === 'fulfilled' && !failLease) return outcome.accepted;
|
|
2359
|
+
await this.options.beforeFrameFailureTermination?.();
|
|
2360
|
+
await this.handleFrameFailure(
|
|
2361
|
+
subscription.lease.slot,
|
|
2362
|
+
outstanding.session,
|
|
2363
|
+
outstanding.incarnationGeneration,
|
|
2364
|
+
subscription.lease,
|
|
2365
|
+
);
|
|
2366
|
+
return outcome.status === 'fulfilled' ? outcome.accepted : false;
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
private validateFrameAcknowledgement(
|
|
2370
|
+
value: unknown,
|
|
2371
|
+
exact = false,
|
|
2372
|
+
): plugins.smartpuppeteer.ILiveBrowserFrameAcknowledgementRequest {
|
|
2373
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
2374
|
+
throw new BrowserRuntimeError('INVALID_INPUT');
|
|
2375
|
+
}
|
|
2376
|
+
const record = exact
|
|
2377
|
+
? validateExactKeys(
|
|
2378
|
+
value,
|
|
2379
|
+
['tabId', 'sequence', 'generation', 'viewportRevision'],
|
|
2380
|
+
'frame acknowledgement',
|
|
2381
|
+
)
|
|
2382
|
+
: value as Record<string, unknown>;
|
|
2383
|
+
const tabId = validateBoundedString(record.tabId, 'tabId', 1, 128);
|
|
2384
|
+
if (tabId !== record.tabId) throw new BrowserRuntimeError('INVALID_INPUT');
|
|
2385
|
+
return {
|
|
2386
|
+
tabId,
|
|
2387
|
+
sequence: validateInteger(record.sequence, 'sequence', 1, Number.MAX_SAFE_INTEGER),
|
|
2388
|
+
generation: validateInteger(record.generation, 'generation', 0, Number.MAX_SAFE_INTEGER),
|
|
2389
|
+
viewportRevision: validateInteger(
|
|
2390
|
+
record.viewportRevision,
|
|
2391
|
+
'viewportRevision',
|
|
2392
|
+
1,
|
|
2393
|
+
Number.MAX_SAFE_INTEGER,
|
|
2394
|
+
),
|
|
2395
|
+
};
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
private frameAcknowledgementKey(
|
|
2399
|
+
acknowledgement: plugins.smartpuppeteer.ILiveBrowserFrameAcknowledgementRequest,
|
|
2400
|
+
): string {
|
|
2401
|
+
return `${acknowledgement.tabId.length}:${acknowledgement.tabId}`
|
|
2402
|
+
+ `:${acknowledgement.sequence}:${acknowledgement.generation}`
|
|
2403
|
+
+ `:${acknowledgement.viewportRevision}`;
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
private async waitForFrameAcknowledgement(
|
|
2407
|
+
session: ILiveBrowserSessionLike,
|
|
2408
|
+
acknowledgement: plugins.smartpuppeteer.ILiveBrowserFrameAcknowledgementRequest,
|
|
2409
|
+
): Promise<TFrameAcknowledgementOutcome> {
|
|
2410
|
+
let operation: Promise<plugins.smartpuppeteer.ILiveBrowserFrameAcknowledgement>;
|
|
2411
|
+
try {
|
|
2412
|
+
operation = session.acknowledgeFrame(acknowledgement);
|
|
2413
|
+
} catch {
|
|
2414
|
+
return { status: 'rejected' };
|
|
2415
|
+
}
|
|
2416
|
+
try {
|
|
2417
|
+
const result = await waitBounded(operation, this.options.frameAcknowledgementTimeoutMs);
|
|
2418
|
+
if (!result.settled) {
|
|
2419
|
+
operation.catch(() => undefined);
|
|
2420
|
+
return { status: 'timedOut' };
|
|
2031
2421
|
}
|
|
2422
|
+
return { status: 'fulfilled', accepted: result.value.accepted };
|
|
2423
|
+
} catch {
|
|
2424
|
+
return { status: 'rejected' };
|
|
2032
2425
|
}
|
|
2033
2426
|
}
|
|
2034
2427
|
|
|
@@ -2038,13 +2431,10 @@ export class BrowserRuntime {
|
|
|
2038
2431
|
acknowledgement: plugins.smartpuppeteer.ILiveBrowserFrameAcknowledgementRequest,
|
|
2039
2432
|
failLease = false,
|
|
2040
2433
|
): Promise<void> {
|
|
2041
|
-
const failedLease = slot.session === session ? slot.lease :
|
|
2434
|
+
const failedLease = slot.session === session ? (slot.lease ?? null) : null;
|
|
2042
2435
|
const incarnationGeneration = slot.incarnationGeneration;
|
|
2043
|
-
const
|
|
2044
|
-
|
|
2045
|
-
.catch(() => ({ settled: true as const, value: { accepted: false } }));
|
|
2046
|
-
if (result.settled && result.value.accepted && !failLease) return;
|
|
2047
|
-
operation.catch(() => undefined);
|
|
2436
|
+
const outcome = await this.waitForFrameAcknowledgement(session, acknowledgement);
|
|
2437
|
+
if (outcome.status === 'fulfilled' && !failLease) return;
|
|
2048
2438
|
await this.options.beforeFrameFailureTermination?.();
|
|
2049
2439
|
await this.handleFrameFailure(slot, session, incarnationGeneration, failedLease);
|
|
2050
2440
|
}
|
|
@@ -2053,27 +2443,43 @@ export class BrowserRuntime {
|
|
|
2053
2443
|
slot: IResourceSlot,
|
|
2054
2444
|
session: ILiveBrowserSessionLike,
|
|
2055
2445
|
incarnationGeneration: number,
|
|
2056
|
-
|
|
2446
|
+
expectedLease: ILeaseRecord | null,
|
|
2057
2447
|
): Promise<void> {
|
|
2058
2448
|
let transitionGeneration = 0;
|
|
2059
2449
|
let ownsTransitionFence = false;
|
|
2450
|
+
const lease = expectedLease ?? undefined;
|
|
2060
2451
|
const release = await slot.mutex.acquire();
|
|
2061
2452
|
try {
|
|
2062
|
-
|
|
2453
|
+
const exactLeaseIsCurrent = expectedLease === null
|
|
2454
|
+
? slot.lease === undefined
|
|
2455
|
+
: slot.lease === expectedLease && expectedLease.capability.lease === expectedLease;
|
|
2456
|
+
let leaseAttachmentIsCurrent = true;
|
|
2457
|
+
if (lease) {
|
|
2458
|
+
try {
|
|
2459
|
+
this.assertCapabilityStillAttached(lease.capability, slot);
|
|
2460
|
+
} catch {
|
|
2461
|
+
leaseAttachmentIsCurrent = false;
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2063
2464
|
const borrowsLeaseReleaseFence = Boolean(
|
|
2064
|
-
lease
|
|
2465
|
+
lease
|
|
2466
|
+
&& exactLeaseIsCurrent
|
|
2467
|
+
&& lease.releaseGeneration !== undefined
|
|
2065
2468
|
&& slot.fencingGeneration === lease.releaseGeneration,
|
|
2066
2469
|
);
|
|
2067
2470
|
if (
|
|
2068
2471
|
slot.session !== session
|
|
2069
2472
|
|| slot.incarnationGeneration !== incarnationGeneration
|
|
2070
|
-
||
|
|
2071
|
-
||
|
|
2473
|
+
|| slot.permanentlyFenced
|
|
2474
|
+
|| slot.retirementPending
|
|
2475
|
+
|| slot.terminationFenceGeneration !== undefined
|
|
2476
|
+
|| slot.attachmentFenceCount > 0
|
|
2072
2477
|
|| slot.attachmentRetryBinding
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2478
|
+
|| !exactLeaseIsCurrent
|
|
2479
|
+
|| !leaseAttachmentIsCurrent
|
|
2480
|
+
|| (slot.fencingGeneration !== undefined && !borrowsLeaseReleaseFence)
|
|
2481
|
+
) return;
|
|
2482
|
+
if (lease) this.invalidateCapabilityRecord(lease.capability);
|
|
2077
2483
|
if (borrowsLeaseReleaseFence) {
|
|
2078
2484
|
transitionGeneration = slot.fencingGeneration!;
|
|
2079
2485
|
} else {
|
|
@@ -2848,13 +3254,19 @@ export class BrowserRuntime {
|
|
|
2848
3254
|
const channelId = value.channelId === undefined
|
|
2849
3255
|
? undefined
|
|
2850
3256
|
: this.validateIdentifier(value.channelId, 'channelId');
|
|
3257
|
+
const runId = value.runId === undefined
|
|
3258
|
+
? undefined
|
|
3259
|
+
: this.validateIdentifier(value.runId, 'runId');
|
|
2851
3260
|
if (
|
|
2852
3261
|
(role === 'agent' && !sessionId)
|
|
2853
3262
|
|| (role === 'human' && sessionId !== undefined)
|
|
2854
|
-
|| (source === 'flex' && (!scopeId || !channelId ||
|
|
3263
|
+
|| (source === 'flex' && (!scopeId || !channelId || !runId
|
|
3264
|
+
|| sessionId?.harnessId !== 'flex'))
|
|
2855
3265
|
|| (source === 'mcp' && (scopeId !== undefined || channelId !== undefined
|
|
3266
|
+
|| runId !== undefined
|
|
2856
3267
|
|| sessionId?.harnessId !== 'opencode'))
|
|
2857
|
-
|| (source === 'human' && (scopeId !== undefined || channelId !== undefined
|
|
3268
|
+
|| (source === 'human' && (scopeId !== undefined || channelId !== undefined
|
|
3269
|
+
|| runId !== undefined))
|
|
2858
3270
|
) throw new BrowserRuntimeError('INVALID_INPUT');
|
|
2859
3271
|
const base = {
|
|
2860
3272
|
projectId: this.validateIdentifier(value.projectId, 'projectId'),
|
|
@@ -2876,7 +3288,15 @@ export class BrowserRuntime {
|
|
|
2876
3288
|
return { ...base, role, source };
|
|
2877
3289
|
}
|
|
2878
3290
|
if (role === 'agent' && source === 'flex' && sessionId?.harnessId === 'flex') {
|
|
2879
|
-
return {
|
|
3291
|
+
return {
|
|
3292
|
+
...base,
|
|
3293
|
+
role,
|
|
3294
|
+
source,
|
|
3295
|
+
sessionId,
|
|
3296
|
+
scopeId: scopeId!,
|
|
3297
|
+
channelId: channelId!,
|
|
3298
|
+
runId: runId!,
|
|
3299
|
+
};
|
|
2880
3300
|
}
|
|
2881
3301
|
if (role === 'agent' && source === 'mcp' && sessionId?.harnessId === 'opencode') {
|
|
2882
3302
|
return { ...base, role, source, sessionId };
|
|
@@ -2910,6 +3330,7 @@ export class BrowserRuntime {
|
|
|
2910
3330
|
sessionId: capability.sessionId,
|
|
2911
3331
|
scopeId: capability.scopeId!,
|
|
2912
3332
|
channelId: capability.channelId!,
|
|
3333
|
+
runId: capability.runId!,
|
|
2913
3334
|
};
|
|
2914
3335
|
}
|
|
2915
3336
|
if (
|
|
@@ -2941,6 +3362,7 @@ export class BrowserRuntime {
|
|
|
2941
3362
|
&& left.source === right.source
|
|
2942
3363
|
&& left.scopeId === right.scopeId
|
|
2943
3364
|
&& left.channelId === right.channelId
|
|
3365
|
+
&& left.runId === right.runId
|
|
2944
3366
|
&& this.sessionsEqual(left.sessionId, right.sessionId);
|
|
2945
3367
|
}
|
|
2946
3368
|
|
|
@@ -3044,6 +3466,14 @@ export class BrowserRuntimeLease {
|
|
|
3044
3466
|
return this.runtime.getLeaseState(this.record);
|
|
3045
3467
|
}
|
|
3046
3468
|
|
|
3469
|
+
public getAuthority(): TBrowserRuntimeLeaseAuthority {
|
|
3470
|
+
return this.runtime.getLeaseAuthority(this.record);
|
|
3471
|
+
}
|
|
3472
|
+
|
|
3473
|
+
public isAuthorityCurrent(authority: TBrowserRuntimeLeaseAuthority): boolean {
|
|
3474
|
+
return this.runtime.isLeaseAuthorityCurrent(this.record, authority);
|
|
3475
|
+
}
|
|
3476
|
+
|
|
3047
3477
|
public executeAgentAction(
|
|
3048
3478
|
action: TBrowserAgentAction,
|
|
3049
3479
|
options?: IBrowserRuntimeOperationOptions,
|