agl 22.0.4 → 24.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.
Files changed (44) hide show
  1. package/changelog.md +21 -0
  2. package/dist_serve/bundle.js +2418 -2408
  3. package/dist_serve/bundle.js.map +1 -0
  4. package/dist_ts/00_commitinfo_data.js +1 -1
  5. package/dist_ts/classes.authstore.js +2 -2
  6. package/dist_ts/classes.browserresourcehost.d.ts +1 -0
  7. package/dist_ts/classes.browserresourcehost.js +7 -1
  8. package/dist_ts/classes.controller.d.ts +37 -5
  9. package/dist_ts/classes.controller.js +1491 -249
  10. package/dist_ts/classes.flexservice.d.ts +5 -0
  11. package/dist_ts/classes.flexservice.js +131 -4
  12. package/dist_ts/classes.opencodeclient.d.ts +2 -0
  13. package/dist_ts/classes.opencodeclient.js +23 -1
  14. package/dist_ts/classes.validation.d.ts +9 -1
  15. package/dist_ts/classes.validation.js +62 -7
  16. package/dist_ts/interfaces.flexipc.d.ts +18 -1
  17. package/dist_ts/interfaces.flexipc.js +42 -8
  18. package/dist_ts_interfaces/browsertransport.d.ts +72 -0
  19. package/dist_ts_interfaces/browsertransport.js +47 -0
  20. package/dist_ts_interfaces/index.d.ts +1 -0
  21. package/dist_ts_interfaces/index.js +2 -1
  22. package/dist_ts_interfaces/interfaces.d.ts +42 -7
  23. package/dist_ts_interfaces/interfaces.js +5 -3
  24. package/dist_ts_interfaces/requests.d.ts +20 -21
  25. package/package.json +7 -7
  26. package/readme.md +12 -2
  27. package/ts/00_commitinfo_data.ts +1 -1
  28. package/ts/classes.authstore.ts +1 -1
  29. package/ts/classes.browserresourcehost.ts +7 -0
  30. package/ts/classes.controller.ts +1960 -305
  31. package/ts/classes.flexservice.ts +132 -2
  32. package/ts/classes.opencodeclient.ts +19 -0
  33. package/ts/classes.validation.ts +77 -6
  34. package/ts/interfaces.flexipc.ts +65 -7
  35. package/ts_interfaces/browsertransport.ts +126 -0
  36. package/ts_interfaces/index.ts +1 -0
  37. package/ts_interfaces/interfaces.ts +46 -11
  38. package/ts_interfaces/requests.ts +23 -30
  39. package/ts_web/00_commitinfo_data.ts +1 -1
  40. package/ts_web/classes.browserviewtransport.ts +532 -0
  41. package/ts_web/classes.controllersocketclient.ts +119 -29
  42. package/ts_web/elements.harnesscontrollerapp.ts +1727 -287
  43. package/ts_web/index.ts +1 -0
  44. package/ts_web/plugins.ts +2 -0
@@ -12,6 +12,22 @@ import {
12
12
  controllerBuiltinCommands,
13
13
  controllerMaxToolEventBytes,
14
14
  controllerMaxLiveMessageEventBytes,
15
+ controllerBrowserTransportEventContentType,
16
+ controllerBrowserTransportMaximumActiveAcknowledgements,
17
+ controllerBrowserTransportMaximumFrameBytes,
18
+ controllerBrowserTransportMaximumJsonBytes,
19
+ controllerBrowserTransportMaximumPendingEventBytes,
20
+ controllerBrowserTransportMaximumPendingEvents,
21
+ controllerBrowserTransportMaximumPendingOperations,
22
+ controllerBrowserTransportMaximumQueuedOperationBytes,
23
+ controllerBrowserTransportMaximumRecoveryAttempts,
24
+ controllerBrowserTransportOperationContentType,
25
+ controllerBrowserTransportPairOpenTimeoutMs,
26
+ controllerBrowserTransportProtocol,
27
+ controllerBrowserTransportRecoveryGraceMs,
28
+ decodeControllerBrowserTransportPacket,
29
+ encodeControllerBrowserTransportPacket,
30
+ controllerLiveMessageDeltaTargetBytes,
15
31
  controllerInitialMessageBundleLimit,
16
32
  controllerMaximumTranscriptBytes,
17
33
  controllerLiveToolPayloadNotice,
@@ -26,7 +42,9 @@ import {
26
42
  type IControllerMessagePage,
27
43
  type IControllerMessageStreamCursor,
28
44
  type IControllerPendingPrompt,
45
+ type IControllerReasoningDelta,
29
46
  type IControllerReasoningUpdate,
47
+ type IControllerTextDelta,
30
48
  type IControllerTextUpdate,
31
49
  type IControllerTodo,
32
50
  type TControllerModelChoice,
@@ -92,8 +110,15 @@ import {
92
110
  type IReq_ControllerBrowserViewOpen,
93
111
  type IReq_ControllerBrowserViewActivate,
94
112
  type IReq_ControllerBrowserViewClose,
95
- type IReq_ControllerBrowserViewOperate,
96
- type IReq_ControllerBrowserViewEvent,
113
+ type IReq_ControllerBrowserViewRecover,
114
+ type IControllerBrowserFrame,
115
+ type IControllerBrowserFrameMetadata,
116
+ type IControllerBrowserViewState,
117
+ type IControllerBrowserOperationPacket,
118
+ type IControllerBrowserOperationResultPacket,
119
+ type TControllerBrowserClientPacket,
120
+ type TControllerBrowserServerPacket,
121
+ type TControllerBrowserViewOperation,
97
122
  type IReq_ControllerProjectCreate,
98
123
  type IReq_ControllerProjectList,
99
124
  type IReq_ControllerProjectRemove,
@@ -239,7 +264,8 @@ import {
239
264
  assertBrowserViewOpenRequest,
240
265
  assertBrowserViewActivateRequest,
241
266
  assertBrowserViewCloseRequest,
242
- assertBrowserViewOperateRequest,
267
+ assertBrowserViewRecoverRequest,
268
+ assertControllerBrowserClientPacket,
243
269
  assertProjectCreateRequest,
244
270
  resolveProjectPathInput,
245
271
  assertProjectRemoveRequest,
@@ -329,9 +355,12 @@ const trustedProxySecretHeader = 'x-harness-controller-proxy-secret';
329
355
  const websocketPayloadLimit = 16 * 1024 * 1024;
330
356
  const browserViewActivationTimeoutMs = 15_000;
331
357
  const browserViewCleanupRetryMs = 1_000;
332
- const maxPendingBrowserViewEvents = 16;
333
- const maxPendingBrowserViewEventBytes = 16 * 1024 * 1024;
334
- const browserFrameStreamAuthorizationMarker = Symbol('controller.browser.frame.stream');
358
+ const browserViewTransportCleanupTimeoutMs = 9_000;
359
+ const browserViewRealtimePeerRate = 120;
360
+ const browserViewRealtimePeerBurst = 240;
361
+ const browserViewRealtimeGlobalRate = 512;
362
+ const browserViewRealtimeGlobalBurst = 1_024;
363
+ const browserStreamAuthorizationMarker = Symbol('controller.browser.stream');
335
364
  const browserSemanticOperationClassifications: ReadonlySet<
336
365
  plugins.browserRuntime.TBrowserRuntimeOperationClassification
337
366
  > = new Set(['navigation', 'tab', 'agent-action']);
@@ -473,22 +502,122 @@ interface ITerminalPeerLifecycleState {
473
502
  connected: boolean;
474
503
  }
475
504
 
505
+ class ControllerBrowserTokenBucket {
506
+ private tokens: number;
507
+ private updatedAt = performance.now();
508
+
509
+ constructor(
510
+ private readonly refillPerSecond: number,
511
+ private readonly capacity: number,
512
+ ) {
513
+ this.tokens = capacity;
514
+ }
515
+
516
+ public consume(nowArg = performance.now()): boolean {
517
+ const elapsedMs = Math.max(0, nowArg - this.updatedAt);
518
+ this.updatedAt = nowArg;
519
+ this.tokens = Math.min(
520
+ this.capacity,
521
+ this.tokens + ((elapsedMs / 1_000) * this.refillPerSecond),
522
+ );
523
+ if (this.tokens < 1) return false;
524
+ this.tokens -= 1;
525
+ return true;
526
+ }
527
+ }
528
+
529
+ type TControllerBrowserViewLifecycle =
530
+ | 'inactive'
531
+ | 'activating'
532
+ | 'active'
533
+ | 'recoverable'
534
+ | 'recovering'
535
+ | 'failing'
536
+ | 'closing';
537
+
538
+ type TControllerBrowserTransportLifecycle =
539
+ | 'opening'
540
+ | 'ready'
541
+ | 'active'
542
+ | 'failed'
543
+ | 'retiring'
544
+ | 'retired';
545
+
546
+ interface IControllerBrowserQueuedOperation {
547
+ operationId: number;
548
+ operation: TControllerBrowserViewOperation;
549
+ byteLength: number;
550
+ abortController?: AbortController;
551
+ cancelled: boolean;
552
+ terminal: boolean;
553
+ }
554
+
555
+ interface IControllerBrowserAcknowledgementOperation {
556
+ operationId: number;
557
+ terminal: boolean;
558
+ operation: Promise<void>;
559
+ }
560
+
561
+ interface IControllerBrowserViewTransport {
562
+ readonly identity: symbol;
563
+ generation: number;
564
+ lifecycle: TControllerBrowserTransportLifecycle;
565
+ events: plugins.typedrequestInterfaces.TVirtualStream<'send'>;
566
+ operations: plugins.typedrequestInterfaces.TVirtualStream<'receive'>;
567
+ abortController: AbortController;
568
+ openingTask: Promise<void>;
569
+ receiveTask?: Promise<void>;
570
+ drainTask?: Promise<void>;
571
+ deliveryTail: Promise<void>;
572
+ pendingEventCount: number;
573
+ pendingEventBytes: number;
574
+ operationQueue: IControllerBrowserQueuedOperation[];
575
+ queuedOperationBytes: number;
576
+ highestOperationId: number;
577
+ cancellableOperationIds: Set<number>;
578
+ activeMutation?: IControllerBrowserQueuedOperation;
579
+ activeAcknowledgements: Map<number, IControllerBrowserAcknowledgementOperation>;
580
+ retirementTask?: Promise<void>;
581
+ retirementSettlement?: Promise<void>;
582
+ retirementSettled?: boolean;
583
+ retirementDeadlineExpired?: boolean;
584
+ failure?: unknown;
585
+ }
586
+
587
+ interface IControllerBrowserTransportConstructionCleanup {
588
+ readonly settlement: Promise<void>;
589
+ readonly reason: unknown;
590
+ settled: boolean;
591
+ deadlineExpired: boolean;
592
+ }
593
+
476
594
  interface IControllerBrowserView {
477
595
  id: string;
478
596
  peerId: string;
597
+ credentialId: string;
479
598
  transportConnection: plugins.typedsocket.ISmartServeConnectionWrapper;
480
599
  transportPeer: plugins.typedsocket.ISmartServeConnectionWrapper['peer'];
481
600
  resource: IControllerResourceDocument;
482
601
  handle: Awaited<ReturnType<ControllerBrowserResourceHost['openHumanView']>>;
483
602
  authority: plugins.browserRuntime.TBrowserRuntimeLeaseAuthority;
484
- activationState: 'inactive' | 'activating' | 'active';
603
+ lifecycle: TControllerBrowserViewLifecycle;
485
604
  activationAbortController: AbortController;
486
605
  activationTimer?: ReturnType<typeof setTimeout>;
487
606
  activationPromise?: Promise<void>;
488
- deliveryTail: Promise<void>;
489
- pendingEventCount: number;
490
- pendingEventBytes: number;
491
- activeFrameStreams: Set<plugins.typedrequestInterfaces.TVirtualStream<'send'>>;
607
+ transport?: IControllerBrowserViewTransport;
608
+ pendingTransportIdentity?: symbol;
609
+ transportConstructionCleanup?: IControllerBrowserTransportConstructionCleanup;
610
+ streamGeneration: number;
611
+ stateRevision: number;
612
+ recoveryAttempts: number;
613
+ recoveryDeadlineAt?: number;
614
+ recoveryTimer?: ReturnType<typeof setTimeout>;
615
+ recoveryFrameGate?: {
616
+ tabId: string;
617
+ minimumGeneration: number;
618
+ viewportRevision: number;
619
+ admitted: boolean;
620
+ };
492
621
  closing: boolean;
493
622
  cleanupTask?: Promise<void>;
494
623
  cleanupRetryTimer?: ReturnType<typeof setTimeout>;
@@ -500,13 +629,15 @@ interface IControllerBrowserViewSlot {
500
629
  view?: IControllerBrowserView;
501
630
  }
502
631
 
503
- interface IControllerBrowserFrameStreamAuthorization {
504
- readonly marker: typeof browserFrameStreamAuthorizationMarker;
632
+ interface IControllerBrowserStreamAuthorization {
633
+ readonly marker: typeof browserStreamAuthorizationMarker;
505
634
  readonly view: IControllerBrowserView;
506
635
  readonly authority: plugins.browserRuntime.TBrowserRuntimeLeaseAuthority;
507
636
  readonly transportPeer: plugins.typedsocket.ISmartServeConnectionWrapper['peer'];
637
+ readonly transportIdentity: symbol;
638
+ readonly streamGeneration: number;
639
+ readonly creatorDirection: plugins.typedrequestInterfaces.TVirtualStreamDirection;
508
640
  readonly contentType: string;
509
- readonly integrity: plugins.typedrequestInterfaces.IVirtualStreamIntegrity;
510
641
  }
511
642
 
512
643
  interface IFlexRefreshJobCacheEntry {
@@ -540,15 +671,24 @@ interface ILatestToolExecution {
540
671
  interface ILatestReasoningUpdate {
541
672
  projectId: string;
542
673
  update: IControllerReasoningUpdate;
674
+ textUtf8Bytes: number;
543
675
  bytes?: number;
544
676
  }
545
677
 
546
678
  interface ILatestTextUpdate {
547
679
  projectId: string;
548
680
  update: IControllerTextUpdate;
681
+ textUtf8Bytes: number;
549
682
  bytes?: number;
550
683
  }
551
684
 
685
+ type TControllerMessageDelta = IControllerReasoningDelta | IControllerTextDelta;
686
+
687
+ interface IControllerTextDeltaFragment {
688
+ delta: string;
689
+ utf8Bytes: number;
690
+ }
691
+
552
692
  type TControllerToolExecutionSnapshot = Omit<
553
693
  IControllerToolExecution,
554
694
  'revision' | 'streamEpoch'
@@ -558,6 +698,103 @@ const historyLimitedNotice = '[earlier message content omitted: this transcript
558
698
  const historyLimitedDigestPrefix = 'history-limited:';
559
699
  const liveToolExecutionBytes = controllerMaxToolEventBytes - (8 * 1024);
560
700
 
701
+ const controllerEventMessageDelta = (
702
+ eventArg: IControllerEvent,
703
+ ): TControllerMessageDelta | undefined => eventArg.reasoningDelta ?? eventArg.textDelta;
704
+
705
+ const controllerTextAfterAppendUtf8Bytes = (
706
+ textArg: string,
707
+ textUtf8BytesArg: number,
708
+ deltaArg: string,
709
+ ): number | undefined => {
710
+ // Separately encoded surrogate halves cost six bytes; after append the pair costs four.
711
+ const boundaryCorrection = (
712
+ textArg.length > 0
713
+ && deltaArg.length > 0
714
+ && textArg.charCodeAt(textArg.length - 1) >= 0xD800
715
+ && textArg.charCodeAt(textArg.length - 1) <= 0xDBFF
716
+ && deltaArg.charCodeAt(0) >= 0xDC00
717
+ && deltaArg.charCodeAt(0) <= 0xDFFF
718
+ ) ? -2 : 0;
719
+ const result = textUtf8BytesArg + Buffer.byteLength(deltaArg, 'utf8') + boundaryCorrection;
720
+ return Number.isSafeInteger(result) && result >= 0 ? result : undefined;
721
+ };
722
+
723
+ const fragmentControllerTextDelta = (valueArg: string): IControllerTextDeltaFragment[] => {
724
+ const fragments: IControllerTextDeltaFragment[] = [];
725
+ let delta = '';
726
+ let utf8Bytes = 0;
727
+ for (const codePoint of valueArg) {
728
+ const codePointBytes = Buffer.byteLength(codePoint, 'utf8');
729
+ if (
730
+ delta !== ''
731
+ && utf8Bytes + codePointBytes > controllerLiveMessageDeltaTargetBytes
732
+ ) {
733
+ fragments.push({ delta, utf8Bytes });
734
+ delta = '';
735
+ utf8Bytes = 0;
736
+ }
737
+ delta += codePoint;
738
+ utf8Bytes += codePointBytes;
739
+ }
740
+ if (delta !== '') fragments.push({ delta, utf8Bytes });
741
+ return fragments;
742
+ };
743
+
744
+ const controllerMessageDeltaPendingKey = (eventArg: IControllerEvent): string | undefined => {
745
+ const delta = controllerEventMessageDelta(eventArg);
746
+ if (!delta) return undefined;
747
+ return JSON.stringify([
748
+ eventArg.type,
749
+ eventArg.harnessId ?? null,
750
+ eventArg.projectId ?? null,
751
+ controllerRuntimeIdKey(delta.sessionId),
752
+ controllerRuntimeIdKey(delta.partId),
753
+ ]);
754
+ };
755
+
756
+ const controllerMessageDeltaEventsAreContiguous = (
757
+ previousEventArg: IControllerEvent,
758
+ eventArg: IControllerEvent,
759
+ ): boolean => {
760
+ const previous = controllerEventMessageDelta(previousEventArg);
761
+ const delta = controllerEventMessageDelta(eventArg);
762
+ if (!previous || !delta) return false;
763
+ return previousEventArg.type === eventArg.type
764
+ && previousEventArg.harnessId === eventArg.harnessId
765
+ && previousEventArg.projectId === eventArg.projectId
766
+ && (previousEventArg.sessionId
767
+ ? eventArg.sessionId !== undefined
768
+ && controllerRuntimeIdKey(previousEventArg.sessionId)
769
+ === controllerRuntimeIdKey(eventArg.sessionId)
770
+ : eventArg.sessionId === undefined)
771
+ && controllerRuntimeIdKey(previous.sessionId) === controllerRuntimeIdKey(delta.sessionId)
772
+ && controllerRuntimeIdKey(previous.messageId) === controllerRuntimeIdKey(delta.messageId)
773
+ && controllerRuntimeIdKey(previous.partId) === controllerRuntimeIdKey(delta.partId)
774
+ && previous.streamEpoch === delta.streamEpoch
775
+ && previous.order?.messageIndex === delta.order?.messageIndex
776
+ && previous.order?.partIndex === delta.order?.partIndex
777
+ && previous.textUtf8Bytes === delta.baseTextUtf8Bytes
778
+ && delta.revision > previous.revision;
779
+ };
780
+
781
+ const mergeControllerMessageDeltaEvents = (
782
+ previousEventArg: IControllerEvent,
783
+ eventArg: IControllerEvent,
784
+ ): IControllerEvent | undefined => {
785
+ if (!controllerMessageDeltaEventsAreContiguous(previousEventArg, eventArg)) return undefined;
786
+ const previous = controllerEventMessageDelta(previousEventArg)!;
787
+ const delta = controllerEventMessageDelta(eventArg)!;
788
+ const mergedDelta = {
789
+ ...delta,
790
+ delta: `${previous.delta}${delta.delta}`,
791
+ baseTextUtf8Bytes: previous.baseTextUtf8Bytes,
792
+ };
793
+ return eventArg.reasoningDelta
794
+ ? { ...eventArg, reasoningDelta: mergedDelta }
795
+ : { ...eventArg, textDelta: mergedDelta };
796
+ };
797
+
561
798
  const markHistoryLimitedDigest = (digestArg: string): string => (
562
799
  digestArg.startsWith(historyLimitedDigestPrefix)
563
800
  ? digestArg
@@ -1502,6 +1739,11 @@ export class OpenCodeController {
1502
1739
  private resourceCoordinator?: ControllerResourceCoordinator;
1503
1740
  private readonly browserViews = new Map<string, IControllerBrowserView>();
1504
1741
  private readonly browserViewSlots = new Map<string, IControllerBrowserViewSlot>();
1742
+ private readonly browserViewRealtimeBucketsByPeer = new Map<string, ControllerBrowserTokenBucket>();
1743
+ private readonly browserViewRealtimeGlobalBucket = new ControllerBrowserTokenBucket(
1744
+ browserViewRealtimeGlobalRate,
1745
+ browserViewRealtimeGlobalBurst,
1746
+ );
1505
1747
  private readonly authenticatedPeerIds = new Set<string>();
1506
1748
  private openCodeResourceGeneration = 0;
1507
1749
  private flexResourceGeneration = 0;
@@ -1950,7 +2192,7 @@ export class OpenCodeController {
1950
2192
  headersTimeout: 10_000,
1951
2193
  requestTimeout: 30_000,
1952
2194
  virtualStreamAuthorizationAdapter: {
1953
- bind: (authorizationArg, contextArg) => this.bindBrowserFrameStreamAuthorization(
2195
+ bind: (authorizationArg, contextArg) => this.bindBrowserStreamAuthorization(
1954
2196
  authorizationArg,
1955
2197
  contextArg,
1956
2198
  ),
@@ -2937,6 +3179,7 @@ export class OpenCodeController {
2937
3179
  (terminalPeerLifecycle as ITerminalPeerLifecycleState).connected = false;
2938
3180
  }
2939
3181
  this.authenticatedPeerIds.delete(peerArg.id);
3182
+ this.browserViewRealtimeBucketsByPeer.delete(peerArg.id);
2940
3183
  const cleanupResults = await Promise.allSettled([
2941
3184
  this.terminalManager?.detachPeer(peerArg.id) ?? Promise.resolve(),
2942
3185
  this.closeBrowserViewsForPeer(peerArg.id),
@@ -3134,6 +3377,7 @@ export class OpenCodeController {
3134
3377
  }
3135
3378
  this.resourceCoordinator = undefined;
3136
3379
  this.authenticatedPeerIds.clear();
3380
+ this.browserViewRealtimeBucketsByPeer.clear();
3137
3381
 
3138
3382
  await Promise.allSettled([
3139
3383
  ...(this.openCodeControllerEventDrainTask ? [this.openCodeControllerEventDrainTask] : []),
@@ -5699,43 +5943,52 @@ export class OpenCodeController {
5699
5943
  'controller.browser.view.activate',
5700
5944
  async (requestArg, toolsArg) => {
5701
5945
  const peer = peerFromLocalData(toolsArg?.localData);
5702
- const { viewId } = validateRequest(() => assertBrowserViewActivateRequest(requestArg));
5946
+ const { viewId, streamGeneration } = validateRequest(
5947
+ () => assertBrowserViewActivateRequest(requestArg),
5948
+ );
5703
5949
  return this.runOperation(peer, async (signalArg) => {
5704
- await this.activateBrowserView(viewId, peer.id, signalArg);
5950
+ await this.activateBrowserView(viewId, peer, streamGeneration, signalArg);
5705
5951
  return { activated: true as const };
5706
5952
  }, toolsArg?.abortSignal);
5707
5953
  },
5708
5954
  ));
5709
5955
 
5956
+ protectedRouter.addTypedHandler(new plugins.typedrequest.TypedHandler<IReq_ControllerBrowserViewRecover>(
5957
+ 'controller.browser.view.recover',
5958
+ async (requestArg, toolsArg) => {
5959
+ const peer = peerFromLocalData(toolsArg?.localData);
5960
+ const transportConnection = this.requireTypedServer().getServerConnectionForRequest(toolsArg);
5961
+ const parsed = validateRequest(() => assertBrowserViewRecoverRequest(requestArg));
5962
+ return this.runOperation(peer, async (signalArg) => this.recoverBrowserView(
5963
+ peer,
5964
+ transportConnection,
5965
+ parsed,
5966
+ signalArg,
5967
+ ), toolsArg?.abortSignal);
5968
+ },
5969
+ ));
5970
+
5710
5971
  protectedRouter.addTypedHandler(new plugins.typedrequest.TypedHandler<IReq_ControllerBrowserViewClose>(
5711
5972
  'controller.browser.view.close',
5712
5973
  async (requestArg, toolsArg) => {
5713
5974
  const peer = peerFromLocalData(toolsArg?.localData);
5714
- const { viewId } = validateRequest(() => assertBrowserViewCloseRequest(requestArg));
5975
+ const { viewId, expectedStreamGeneration } = validateRequest(
5976
+ () => assertBrowserViewCloseRequest(requestArg),
5977
+ );
5715
5978
  return this.runOperation(peer, async () => this.runAudited(
5716
5979
  { type: 'resource.browser.view.close', peerId: peer.id },
5717
5980
  async () => {
5718
- await this.closeBrowserView(viewId, peer.id);
5719
- return { closed: true as const };
5981
+ const closed = await this.closeBrowserView(
5982
+ viewId,
5983
+ peer,
5984
+ expectedStreamGeneration,
5985
+ );
5986
+ return { closed };
5720
5987
  },
5721
5988
  ));
5722
5989
  },
5723
5990
  ));
5724
5991
 
5725
- protectedRouter.addTypedHandler(new plugins.typedrequest.TypedHandler<IReq_ControllerBrowserViewOperate>(
5726
- 'controller.browser.view.operate',
5727
- async (requestArg, toolsArg) => {
5728
- const peer = peerFromLocalData(toolsArg?.localData);
5729
- const parsed = validateRequest(() => assertBrowserViewOperateRequest(requestArg));
5730
- return this.runOperation(peer, async (signalArg) => this.operateBrowserView(
5731
- peer.id,
5732
- parsed.viewId,
5733
- parsed.operation,
5734
- signalArg,
5735
- ), toolsArg?.abortSignal);
5736
- },
5737
- ));
5738
-
5739
5992
  protectedRouter.addTypedHandler(
5740
5993
  new plugins.typedrequest.TypedHandler<IReq_ControllerSessionGroupsGet>(
5741
5994
  'controller.sessiongroups.get',
@@ -9992,6 +10245,10 @@ export class OpenCodeController {
9992
10245
  this.handleFlexTextUpdate(eventArg);
9993
10246
  return;
9994
10247
  }
10248
+ if (eventArg.type === 'part.delta') {
10249
+ this.handleFlexPartDelta(eventArg);
10250
+ return;
10251
+ }
9995
10252
  if (eventArg.type === 'permissions.changed') {
9996
10253
  if (this.autoAcceptPermissions || this.hasFlexYoloSessions()) {
9997
10254
  this.queueFlexAutoAcceptSweep(eventArg.scopeId, eventArg.sessionId);
@@ -10413,7 +10670,12 @@ export class OpenCodeController {
10413
10670
  const bytes = Buffer.byteLength(JSON.stringify(eventArg), 'utf8');
10414
10671
  return {
10415
10672
  tool: eventArg.toolExecution ? bytes : 0,
10416
- message: eventArg.reasoningUpdate || eventArg.textUpdate ? bytes : 0,
10673
+ message: eventArg.reasoningUpdate
10674
+ || eventArg.reasoningDelta
10675
+ || eventArg.textUpdate
10676
+ || eventArg.textDelta
10677
+ ? bytes
10678
+ : 0,
10417
10679
  };
10418
10680
  }
10419
10681
 
@@ -10573,7 +10835,12 @@ export class OpenCodeController {
10573
10835
  this.latestFlexTextUpdates.clear();
10574
10836
  this.latestFlexMessageUpdateBytes = 0;
10575
10837
  for (const [key, event] of this.pendingFlexControllerEvents) {
10576
- if (event.reasoningUpdate || event.textUpdate) this.deletePendingFlexControllerEvent(key);
10838
+ if (
10839
+ event.reasoningUpdate
10840
+ || event.reasoningDelta
10841
+ || event.textUpdate
10842
+ || event.textDelta
10843
+ ) this.deletePendingFlexControllerEvent(key);
10577
10844
  }
10578
10845
  const timestamp = Date.now();
10579
10846
  const barrier: IControllerEvent = {
@@ -10659,11 +10926,19 @@ export class OpenCodeController {
10659
10926
  for (const [key, event] of this.pendingFlexControllerEvents) {
10660
10927
  if (
10661
10928
  event.projectId === projectIdArg
10662
- && (event.toolExecution || event.reasoningUpdate || event.textUpdate)
10929
+ && (
10930
+ event.toolExecution
10931
+ || event.reasoningUpdate
10932
+ || event.reasoningDelta
10933
+ || event.textUpdate
10934
+ || event.textDelta
10935
+ )
10663
10936
  && controllerRuntimeIdKey(
10664
10937
  event.toolExecution?.sessionId
10665
10938
  ?? event.reasoningUpdate?.sessionId
10666
- ?? event.textUpdate!.sessionId,
10939
+ ?? event.reasoningDelta?.sessionId
10940
+ ?? event.textUpdate?.sessionId
10941
+ ?? event.textDelta!.sessionId,
10667
10942
  ) === sessionKey
10668
10943
  ) this.deletePendingFlexControllerEvent(key);
10669
10944
  }
@@ -10729,7 +11004,13 @@ export class OpenCodeController {
10729
11004
  for (const [key, event] of this.pendingFlexControllerEvents) {
10730
11005
  if (
10731
11006
  event.projectId === projectIdArg
10732
- && (event.toolExecution || event.reasoningUpdate || event.textUpdate)
11007
+ && (
11008
+ event.toolExecution
11009
+ || event.reasoningUpdate
11010
+ || event.reasoningDelta
11011
+ || event.textUpdate
11012
+ || event.textDelta
11013
+ )
10733
11014
  ) {
10734
11015
  this.deletePendingFlexControllerEvent(key);
10735
11016
  }
@@ -10886,7 +11167,11 @@ export class OpenCodeController {
10886
11167
  }
10887
11168
  this.setLatestFlexReasoningUpdate(
10888
11169
  key,
10889
- { projectId: eventArg.scopeId, update },
11170
+ {
11171
+ projectId: eventArg.scopeId,
11172
+ update,
11173
+ textUtf8Bytes: Buffer.byteLength(update.text, 'utf8'),
11174
+ },
10890
11175
  eventBytes,
10891
11176
  );
10892
11177
  this.queueFlexControllerEvent(controllerEvent);
@@ -10946,12 +11231,169 @@ export class OpenCodeController {
10946
11231
  }
10947
11232
  this.setLatestFlexTextUpdate(
10948
11233
  key,
10949
- { projectId: eventArg.scopeId, update },
11234
+ {
11235
+ projectId: eventArg.scopeId,
11236
+ update,
11237
+ textUtf8Bytes: Buffer.byteLength(update.text, 'utf8'),
11238
+ },
10950
11239
  eventBytes,
10951
11240
  );
10952
11241
  this.queueFlexControllerEvent(controllerEvent);
10953
11242
  }
10954
11243
 
11244
+ private handleFlexPartDelta(
11245
+ eventArg: Extract<TFlexChildEvent, { type: 'part.delta' }>,
11246
+ ): void {
11247
+ if (this.lifecycleState !== 'starting' && this.lifecycleState !== 'ready') return;
11248
+ const sourceUpdatedAt = Date.parse(eventArg.timestamp);
11249
+ if (!Number.isFinite(sourceUpdatedAt)) {
11250
+ this.advanceFlexMessageStreamEpoch(true);
11251
+ return;
11252
+ }
11253
+ const sessionId = flexRuntimeId(eventArg.sessionId);
11254
+ const messageId = flexRuntimeId(eventArg.messageId);
11255
+ const partId = flexRuntimeId(eventArg.partId);
11256
+ const order = { messageIndex: eventArg.messageIndex, partIndex: eventArg.partIndex };
11257
+ const key = this.openCodeToolExecutionKey(eventArg.scopeId, { sessionId, partId });
11258
+ const cache = eventArg.partType === 'reasoning'
11259
+ ? this.latestFlexReasoningUpdates
11260
+ : this.latestFlexTextUpdates;
11261
+ const entry = cache.get(key);
11262
+ const previous = entry?.update;
11263
+ if (
11264
+ !entry
11265
+ || !previous
11266
+ || this.flexMessageStreamEpoch === 0
11267
+ || entry.projectId !== eventArg.scopeId
11268
+ || previous.status !== 'running'
11269
+ || previous.streamEpoch !== this.flexMessageStreamEpoch
11270
+ || previous.revision > this.flexMessageStreamRevision
11271
+ || controllerRuntimeIdKey(previous.sessionId) !== controllerRuntimeIdKey(sessionId)
11272
+ || controllerRuntimeIdKey(previous.messageId) !== controllerRuntimeIdKey(messageId)
11273
+ || controllerRuntimeIdKey(previous.partId) !== controllerRuntimeIdKey(partId)
11274
+ || previous.order?.messageIndex !== order.messageIndex
11275
+ || previous.order.partIndex !== order.partIndex
11276
+ || sourceUpdatedAt < previous.sourceUpdatedAt
11277
+ || entry.textUtf8Bytes !== eventArg.baseTextUtf8Bytes
11278
+ ) {
11279
+ this.advanceFlexMessageStreamEpoch(true);
11280
+ return;
11281
+ }
11282
+ const textUtf8Bytes = controllerTextAfterAppendUtf8Bytes(
11283
+ previous.text,
11284
+ entry.textUtf8Bytes,
11285
+ eventArg.delta,
11286
+ );
11287
+ if (
11288
+ textUtf8Bytes === undefined
11289
+ || textUtf8Bytes <= entry.textUtf8Bytes
11290
+ || textUtf8Bytes !== eventArg.textUtf8Bytes
11291
+ ) {
11292
+ this.advanceFlexMessageStreamEpoch(true);
11293
+ return;
11294
+ }
11295
+
11296
+ const revision = this.flexMessageStreamRevision + 1;
11297
+ const update = {
11298
+ ...previous,
11299
+ text: `${previous.text}${eventArg.delta}`,
11300
+ sourceUpdatedAt,
11301
+ revision,
11302
+ streamEpoch: this.flexMessageStreamEpoch,
11303
+ } as IControllerReasoningUpdate | IControllerTextUpdate;
11304
+ const controllerEvent: IControllerEvent = eventArg.partType === 'reasoning'
11305
+ ? {
11306
+ type: 'session.reasoning.delta',
11307
+ harnessId: 'flex',
11308
+ projectId: eventArg.scopeId,
11309
+ sessionId,
11310
+ reasoningDelta: {
11311
+ sessionId,
11312
+ messageId,
11313
+ partId,
11314
+ delta: eventArg.delta,
11315
+ baseTextUtf8Bytes: eventArg.baseTextUtf8Bytes,
11316
+ textUtf8Bytes,
11317
+ order,
11318
+ sourceUpdatedAt,
11319
+ revision,
11320
+ streamEpoch: this.flexMessageStreamEpoch,
11321
+ },
11322
+ timestamp: sourceUpdatedAt,
11323
+ }
11324
+ : {
11325
+ type: 'session.text.delta',
11326
+ harnessId: 'flex',
11327
+ projectId: eventArg.scopeId,
11328
+ sessionId,
11329
+ textDelta: {
11330
+ sessionId,
11331
+ messageId,
11332
+ partId,
11333
+ delta: eventArg.delta,
11334
+ baseTextUtf8Bytes: eventArg.baseTextUtf8Bytes,
11335
+ textUtf8Bytes,
11336
+ order,
11337
+ sourceUpdatedAt,
11338
+ revision,
11339
+ streamEpoch: this.flexMessageStreamEpoch,
11340
+ },
11341
+ timestamp: sourceUpdatedAt,
11342
+ };
11343
+ const snapshotEvent: IControllerEvent = eventArg.partType === 'reasoning'
11344
+ ? {
11345
+ type: 'session.reasoning.updated',
11346
+ harnessId: 'flex',
11347
+ projectId: eventArg.scopeId,
11348
+ sessionId,
11349
+ reasoningUpdate: update as IControllerReasoningUpdate,
11350
+ timestamp: sourceUpdatedAt,
11351
+ }
11352
+ : {
11353
+ type: 'session.text.updated',
11354
+ harnessId: 'flex',
11355
+ projectId: eventArg.scopeId,
11356
+ sessionId,
11357
+ textUpdate: update as IControllerTextUpdate,
11358
+ timestamp: sourceUpdatedAt,
11359
+ };
11360
+ const eventBytes = Buffer.byteLength(JSON.stringify(controllerEvent), 'utf8');
11361
+ const snapshotBytes = Buffer.byteLength(JSON.stringify(snapshotEvent), 'utf8');
11362
+ if (
11363
+ eventBytes > controllerMaxLiveMessageEventBytes
11364
+ || snapshotBytes > controllerMaxLiveMessageEventBytes
11365
+ || this.latestFlexMessageUpdateBytes - (entry.bytes ?? 0) + snapshotBytes
11366
+ > maxLatestFlexMessageUpdateBytes
11367
+ ) {
11368
+ this.advanceFlexMessageStreamEpoch(true);
11369
+ return;
11370
+ }
11371
+
11372
+ this.flexMessageStreamRevision = revision;
11373
+ if (eventArg.partType === 'reasoning') {
11374
+ this.setLatestFlexReasoningUpdate(
11375
+ key,
11376
+ {
11377
+ projectId: eventArg.scopeId,
11378
+ update: update as IControllerReasoningUpdate,
11379
+ textUtf8Bytes,
11380
+ },
11381
+ snapshotBytes,
11382
+ );
11383
+ } else {
11384
+ this.setLatestFlexTextUpdate(
11385
+ key,
11386
+ {
11387
+ projectId: eventArg.scopeId,
11388
+ update: update as IControllerTextUpdate,
11389
+ textUtf8Bytes,
11390
+ },
11391
+ snapshotBytes,
11392
+ );
11393
+ }
11394
+ this.queueFlexControllerEvent(controllerEvent);
11395
+ }
11396
+
10955
11397
  private getOpenCodeToolStreamCursor(): IControllerToolStreamCursor {
10956
11398
  return {
10957
11399
  streamEpoch: this.openCodeToolStreamEpoch,
@@ -11075,12 +11517,25 @@ export class OpenCodeController {
11075
11517
 
11076
11518
  private advanceOpenCodeMessageStreamEpoch(includeSessionsChangedArg: boolean): void {
11077
11519
  if (this.lifecycleState !== 'starting' && this.lifecycleState !== 'ready') return;
11520
+ const pendingEntries = includeSessionsChangedArg ? 2 : 1;
11521
+ if (
11522
+ this.pendingOpenCodeControllerEvents.size
11523
+ > maxPendingOpenCodeControllerEvents - pendingEntries
11524
+ ) {
11525
+ this.advanceOpenCodeStreams(includeSessionsChangedArg);
11526
+ return;
11527
+ }
11078
11528
  this.openCodeMessageStreamEpoch += 1;
11079
11529
  this.openCodeMessageStreamRevision = 0;
11080
11530
  this.latestOpenCodeReasoningUpdates.clear();
11081
11531
  this.latestOpenCodeTextUpdates.clear();
11082
11532
  for (const [key, event] of this.pendingOpenCodeControllerEvents) {
11083
- if (event.reasoningUpdate || event.textUpdate) this.pendingOpenCodeControllerEvents.delete(key);
11533
+ if (
11534
+ event.reasoningUpdate
11535
+ || event.reasoningDelta
11536
+ || event.textUpdate
11537
+ || event.textDelta
11538
+ ) this.pendingOpenCodeControllerEvents.delete(key);
11084
11539
  }
11085
11540
  const timestamp = Date.now();
11086
11541
  const barrier: IControllerEvent = {
@@ -11167,6 +11622,143 @@ export class OpenCodeController {
11167
11622
  if (!cache.has(key) && cache.size >= maxLatestOpenCodeMessageUpdates) {
11168
11623
  this.advanceOpenCodeMessageStreamEpoch(true);
11169
11624
  }
11625
+ const previousEntry = cache.get(key);
11626
+ const previous = previousEntry?.update;
11627
+ if (
11628
+ previousEntry !== undefined
11629
+ && previous !== undefined
11630
+ && previousEntry.projectId === projectId
11631
+ && previous.streamEpoch === this.openCodeMessageStreamEpoch
11632
+ && previous.revision <= this.openCodeMessageStreamRevision
11633
+ && controllerRuntimeIdKey(previous.sessionId) === controllerRuntimeIdKey(updateArg.sessionId)
11634
+ && controllerRuntimeIdKey(previous.messageId) === controllerRuntimeIdKey(updateArg.messageId)
11635
+ && controllerRuntimeIdKey(previous.partId) === controllerRuntimeIdKey(updateArg.partId)
11636
+ && previous.status === 'running'
11637
+ && updateArg.status === 'running'
11638
+ && updateArg.text.length > previous.text.length
11639
+ && updateArg.text.startsWith(previous.text)
11640
+ && Number.isSafeInteger(previousEntry.textUtf8Bytes)
11641
+ ) {
11642
+ const deltaText = updateArg.text.slice(previous.text.length);
11643
+ const fragments = fragmentControllerTextDelta(deltaText);
11644
+ const textUtf8Bytes = controllerTextAfterAppendUtf8Bytes(
11645
+ previous.text,
11646
+ previousEntry.textUtf8Bytes,
11647
+ deltaText,
11648
+ );
11649
+ const finalRevision = this.openCodeMessageStreamRevision + fragments.length;
11650
+ if (
11651
+ fragments.length === 0
11652
+ || textUtf8Bytes === undefined
11653
+ || textUtf8Bytes <= previousEntry.textUtf8Bytes
11654
+ || !Number.isSafeInteger(finalRevision)
11655
+ ) {
11656
+ this.advanceOpenCodeMessageStreamEpoch(true);
11657
+ return;
11658
+ }
11659
+ const update = {
11660
+ ...updateArg,
11661
+ revision: finalRevision,
11662
+ streamEpoch: this.openCodeMessageStreamEpoch,
11663
+ } as IControllerReasoningUpdate | IControllerTextUpdate;
11664
+ const snapshotEvent: IControllerEvent = typeArg === 'reasoning'
11665
+ ? {
11666
+ type: 'session.reasoning.updated',
11667
+ harnessId: 'opencode',
11668
+ projectId,
11669
+ sessionId: update.sessionId,
11670
+ reasoningUpdate: update as IControllerReasoningUpdate,
11671
+ timestamp: update.sourceUpdatedAt,
11672
+ }
11673
+ : {
11674
+ type: 'session.text.updated',
11675
+ harnessId: 'opencode',
11676
+ projectId,
11677
+ sessionId: update.sessionId,
11678
+ textUpdate: update as IControllerTextUpdate,
11679
+ timestamp: update.sourceUpdatedAt,
11680
+ };
11681
+ const snapshotBytes = Buffer.byteLength(JSON.stringify(snapshotEvent), 'utf8');
11682
+ if (snapshotBytes > controllerMaxLiveMessageEventBytes) {
11683
+ this.advanceOpenCodeMessageStreamEpoch(true);
11684
+ return;
11685
+ }
11686
+
11687
+ const events: IControllerEvent[] = [];
11688
+ let baseTextUtf8Bytes = previousEntry.textUtf8Bytes;
11689
+ for (const [index, fragment] of fragments.entries()) {
11690
+ const fragmentTextUtf8Bytes = index === 0
11691
+ ? controllerTextAfterAppendUtf8Bytes(
11692
+ previous.text,
11693
+ baseTextUtf8Bytes,
11694
+ fragment.delta,
11695
+ )
11696
+ : baseTextUtf8Bytes + fragment.utf8Bytes;
11697
+ if (
11698
+ fragmentTextUtf8Bytes === undefined
11699
+ || !Number.isSafeInteger(fragmentTextUtf8Bytes)
11700
+ || fragmentTextUtf8Bytes <= baseTextUtf8Bytes
11701
+ ) {
11702
+ this.advanceOpenCodeMessageStreamEpoch(true);
11703
+ return;
11704
+ }
11705
+ const revision = this.openCodeMessageStreamRevision + index + 1;
11706
+ const commonDelta = {
11707
+ sessionId: update.sessionId,
11708
+ messageId: update.messageId,
11709
+ partId: update.partId,
11710
+ delta: fragment.delta,
11711
+ baseTextUtf8Bytes,
11712
+ textUtf8Bytes: fragmentTextUtf8Bytes,
11713
+ sourceUpdatedAt: update.sourceUpdatedAt,
11714
+ revision,
11715
+ streamEpoch: this.openCodeMessageStreamEpoch,
11716
+ };
11717
+ const event: IControllerEvent = typeArg === 'reasoning'
11718
+ ? {
11719
+ type: 'session.reasoning.delta',
11720
+ harnessId: 'opencode',
11721
+ projectId,
11722
+ sessionId: update.sessionId,
11723
+ reasoningDelta: commonDelta,
11724
+ timestamp: update.sourceUpdatedAt,
11725
+ }
11726
+ : {
11727
+ type: 'session.text.delta',
11728
+ harnessId: 'opencode',
11729
+ projectId,
11730
+ sessionId: update.sessionId,
11731
+ textDelta: commonDelta,
11732
+ timestamp: update.sourceUpdatedAt,
11733
+ };
11734
+ if (Buffer.byteLength(JSON.stringify(event), 'utf8') > controllerMaxLiveMessageEventBytes) {
11735
+ this.advanceOpenCodeMessageStreamEpoch(true);
11736
+ return;
11737
+ }
11738
+ events.push(event);
11739
+ baseTextUtf8Bytes = fragmentTextUtf8Bytes;
11740
+ }
11741
+ if (baseTextUtf8Bytes !== textUtf8Bytes) {
11742
+ this.advanceOpenCodeMessageStreamEpoch(true);
11743
+ return;
11744
+ }
11745
+
11746
+ this.openCodeMessageStreamRevision = finalRevision;
11747
+ cache.delete(key);
11748
+ cache.set(key, {
11749
+ projectId,
11750
+ update,
11751
+ textUtf8Bytes,
11752
+ bytes: snapshotBytes,
11753
+ } as ILatestReasoningUpdate & ILatestTextUpdate);
11754
+ const streamEpoch = this.openCodeMessageStreamEpoch;
11755
+ for (const event of events) {
11756
+ this.queueOpenCodeControllerEvent(event);
11757
+ if (this.openCodeMessageStreamEpoch !== streamEpoch) return;
11758
+ }
11759
+ return;
11760
+ }
11761
+
11170
11762
  this.openCodeMessageStreamRevision += 1;
11171
11763
  const update = {
11172
11764
  ...updateArg,
@@ -11190,12 +11782,18 @@ export class OpenCodeController {
11190
11782
  textUpdate: update as IControllerTextUpdate,
11191
11783
  timestamp: update.sourceUpdatedAt,
11192
11784
  };
11193
- if (Buffer.byteLength(JSON.stringify(event), 'utf8') > controllerMaxLiveMessageEventBytes) {
11785
+ const eventBytes = Buffer.byteLength(JSON.stringify(event), 'utf8');
11786
+ if (eventBytes > controllerMaxLiveMessageEventBytes) {
11194
11787
  this.advanceOpenCodeMessageStreamEpoch(true);
11195
11788
  return;
11196
11789
  }
11197
11790
  cache.delete(key);
11198
- cache.set(key, { projectId, update } as ILatestReasoningUpdate & ILatestTextUpdate);
11791
+ cache.set(key, {
11792
+ projectId,
11793
+ update,
11794
+ textUtf8Bytes: Buffer.byteLength(update.text, 'utf8'),
11795
+ bytes: eventBytes,
11796
+ } as ILatestReasoningUpdate & ILatestTextUpdate);
11199
11797
  this.queueOpenCodeControllerEvent(event);
11200
11798
  }
11201
11799
 
@@ -11242,6 +11840,7 @@ export class OpenCodeController {
11242
11840
  }
11243
11841
 
11244
11842
  private controllerEventKey(eventArg: IControllerEvent): string {
11843
+ const delta = controllerEventMessageDelta(eventArg);
11245
11844
  return JSON.stringify([
11246
11845
  eventArg.type,
11247
11846
  eventArg.harnessId ?? null,
@@ -11256,6 +11855,18 @@ export class OpenCodeController {
11256
11855
  eventArg.textUpdate
11257
11856
  ? controllerRuntimeIdKey(eventArg.textUpdate.partId)
11258
11857
  : null,
11858
+ delta
11859
+ ? [
11860
+ controllerRuntimeIdKey(delta.messageId),
11861
+ controllerRuntimeIdKey(delta.partId),
11862
+ delta.streamEpoch,
11863
+ delta.order?.messageIndex ?? null,
11864
+ delta.order?.partIndex ?? null,
11865
+ delta.baseTextUtf8Bytes,
11866
+ delta.textUtf8Bytes,
11867
+ delta.revision,
11868
+ ]
11869
+ : null,
11259
11870
  eventArg.toolExecution
11260
11871
  ? eventArg.toolExecution.status === 'completed' || eventArg.toolExecution.status === 'error'
11261
11872
  ? 'terminal'
@@ -11272,15 +11883,59 @@ export class OpenCodeController {
11272
11883
  ]);
11273
11884
  }
11274
11885
 
11275
- private queueOpenCodeControllerEvent(eventArg: IControllerEvent): void {
11276
- if (this.lifecycleState !== 'starting' && this.lifecycleState !== 'ready') return;
11886
+ private latestPendingControllerDelta(
11887
+ queueArg: Map<string, IControllerEvent>,
11888
+ eventArg: IControllerEvent,
11889
+ ): { key: string; event: IControllerEvent } | undefined {
11890
+ const pendingKey = controllerMessageDeltaPendingKey(eventArg);
11891
+ if (!pendingKey) return undefined;
11892
+ let tail: [string, IControllerEvent] | undefined;
11893
+ for (const entry of queueArg) tail = entry;
11894
+ if (!tail || controllerMessageDeltaPendingKey(tail[1]) !== pendingKey) return undefined;
11895
+ return { key: tail[0], event: tail[1] };
11896
+ }
11897
+
11898
+ private queueOpenCodeControllerEvent(eventArg: IControllerEvent): boolean {
11899
+ if (this.lifecycleState !== 'starting' && this.lifecycleState !== 'ready') return false;
11900
+ const delta = controllerEventMessageDelta(eventArg);
11901
+ if (delta) {
11902
+ if (Buffer.byteLength(JSON.stringify(eventArg), 'utf8') > controllerMaxLiveMessageEventBytes) {
11903
+ this.advanceOpenCodeMessageStreamEpoch(true);
11904
+ return false;
11905
+ }
11906
+ const pending = this.latestPendingControllerDelta(
11907
+ this.pendingOpenCodeControllerEvents,
11908
+ eventArg,
11909
+ );
11910
+ if (pending) {
11911
+ const merged = mergeControllerMessageDeltaEvents(pending.event, eventArg);
11912
+ if (!merged) {
11913
+ this.advanceOpenCodeMessageStreamEpoch(true);
11914
+ return false;
11915
+ }
11916
+ if (
11917
+ Buffer.byteLength(JSON.stringify(merged), 'utf8')
11918
+ <= controllerMaxLiveMessageEventBytes
11919
+ ) {
11920
+ this.pendingOpenCodeControllerEvents.set(pending.key, merged);
11921
+ this.startOpenCodeControllerEventDrain();
11922
+ return true;
11923
+ }
11924
+ }
11925
+ }
11277
11926
  const key = this.controllerEventKey(eventArg);
11278
- if (this.pendingOpenCodeControllerEvents.has(key)) {
11927
+ if (delta && this.pendingOpenCodeControllerEvents.has(key)) {
11928
+ this.advanceOpenCodeMessageStreamEpoch(true);
11929
+ return false;
11930
+ }
11931
+ if (!delta && this.pendingOpenCodeControllerEvents.has(key)) {
11279
11932
  this.pendingOpenCodeControllerEvents.delete(key);
11280
11933
  } else if (
11281
11934
  this.pendingOpenCodeControllerEvents.size >= maxPendingOpenCodeControllerEvents
11282
11935
  ) {
11283
- if (eventArg.reasoningUpdate || eventArg.textUpdate) {
11936
+ if (delta) {
11937
+ this.advanceOpenCodeStreams(true);
11938
+ } else if (eventArg.reasoningUpdate || eventArg.textUpdate) {
11284
11939
  this.advanceOpenCodeMessageStreamEpoch(true);
11285
11940
  } else if (eventArg.toolExecution) {
11286
11941
  this.advanceOpenCodeToolStreamEpoch(true);
@@ -11312,10 +11967,11 @@ export class OpenCodeController {
11312
11967
  rebasedEvent,
11313
11968
  );
11314
11969
  }
11315
- return;
11970
+ return false;
11316
11971
  }
11317
11972
  this.pendingOpenCodeControllerEvents.set(key, eventArg);
11318
11973
  this.startOpenCodeControllerEventDrain();
11974
+ return true;
11319
11975
  }
11320
11976
 
11321
11977
  private startOpenCodeControllerEventDrain(): void {
@@ -11382,7 +12038,11 @@ export class OpenCodeController {
11382
12038
  };
11383
12039
  this.setLatestFlexReasoningUpdate(
11384
12040
  this.openCodeToolExecutionKey(eventArg.projectId, update),
11385
- { projectId: eventArg.projectId, update },
12041
+ {
12042
+ projectId: eventArg.projectId,
12043
+ update,
12044
+ textUtf8Bytes: Buffer.byteLength(update.text, 'utf8'),
12045
+ },
11386
12046
  Buffer.byteLength(JSON.stringify(event), 'utf8'),
11387
12047
  );
11388
12048
  return event;
@@ -11401,7 +12061,11 @@ export class OpenCodeController {
11401
12061
  };
11402
12062
  this.setLatestFlexTextUpdate(
11403
12063
  this.openCodeToolExecutionKey(eventArg.projectId, update),
11404
- { projectId: eventArg.projectId, update },
12064
+ {
12065
+ projectId: eventArg.projectId,
12066
+ update,
12067
+ textUtf8Bytes: Buffer.byteLength(update.text, 'utf8'),
12068
+ },
11405
12069
  Buffer.byteLength(JSON.stringify(event), 'utf8'),
11406
12070
  );
11407
12071
  return event;
@@ -11409,10 +12073,51 @@ export class OpenCodeController {
11409
12073
  return eventArg;
11410
12074
  }
11411
12075
 
11412
- private queueFlexControllerEvent(eventArg: IControllerEvent): void {
11413
- if (this.lifecycleState !== 'starting' && this.lifecycleState !== 'ready') return;
12076
+ private queueFlexControllerEvent(eventArg: IControllerEvent): boolean {
12077
+ if (this.lifecycleState !== 'starting' && this.lifecycleState !== 'ready') return false;
12078
+ const delta = controllerEventMessageDelta(eventArg);
12079
+ if (delta) {
12080
+ if (Buffer.byteLength(JSON.stringify(eventArg), 'utf8') > controllerMaxLiveMessageEventBytes) {
12081
+ this.advanceFlexMessageStreamEpoch(true);
12082
+ return false;
12083
+ }
12084
+ const pending = this.latestPendingControllerDelta(
12085
+ this.pendingFlexControllerEvents,
12086
+ eventArg,
12087
+ );
12088
+ if (pending) {
12089
+ const merged = mergeControllerMessageDeltaEvents(pending.event, eventArg);
12090
+ if (!merged) {
12091
+ this.advanceFlexMessageStreamEpoch(true);
12092
+ return false;
12093
+ }
12094
+ const mergedBytes = this.flexControllerEventLiveBytes(merged);
12095
+ if (
12096
+ mergedBytes.message <= controllerMaxLiveMessageEventBytes
12097
+ ) {
12098
+ const pendingBytes = this.flexControllerEventLiveBytes(pending.event);
12099
+ if (
12100
+ this.latestFlexMessageUpdateBytes
12101
+ + this.pendingFlexMessageEventBytes
12102
+ - pendingBytes.message
12103
+ + mergedBytes.message
12104
+ > maxLatestFlexMessageUpdateBytes
12105
+ ) {
12106
+ this.advanceFlexMessageStreamEpoch(true);
12107
+ return false;
12108
+ }
12109
+ this.setPendingFlexControllerEvent(pending.key, merged);
12110
+ this.startFlexControllerEventDrain();
12111
+ return true;
12112
+ }
12113
+ }
12114
+ }
11414
12115
  const key = this.controllerEventKey(eventArg);
11415
- this.deletePendingFlexControllerEvent(key);
12116
+ if (delta && this.pendingFlexControllerEvents.has(key)) {
12117
+ this.advanceFlexMessageStreamEpoch(true);
12118
+ return false;
12119
+ }
12120
+ if (!delta) this.deletePendingFlexControllerEvent(key);
11416
12121
  const liveBytes = this.flexControllerEventLiveBytes(eventArg);
11417
12122
  const exceedsCountBound = this.pendingFlexControllerEvents.size >= maxPendingFlexControllerEvents;
11418
12123
  const exceedsPendingBound = exceedsCountBound
@@ -11427,7 +12132,12 @@ export class OpenCodeController {
11427
12132
  || this.pendingFlexControllerEvents.size > maxPendingFlexControllerEvents - 3;
11428
12133
  if (requiresCountCollapse) {
11429
12134
  this.advanceFlexStreams(true);
11430
- } else if (eventArg.reasoningUpdate || eventArg.textUpdate) {
12135
+ } else if (
12136
+ eventArg.reasoningUpdate
12137
+ || eventArg.reasoningDelta
12138
+ || eventArg.textUpdate
12139
+ || eventArg.textDelta
12140
+ ) {
11431
12141
  this.advanceFlexMessageStreamEpoch(true);
11432
12142
  } else if (eventArg.toolExecution) {
11433
12143
  this.advanceFlexToolStreamEpoch(true);
@@ -11445,10 +12155,11 @@ export class OpenCodeController {
11445
12155
  this.setPendingFlexControllerEvent(this.controllerEventKey(rebasedEvent), rebasedEvent);
11446
12156
  }
11447
12157
  this.startFlexControllerEventDrain();
11448
- return;
12158
+ return false;
11449
12159
  }
11450
12160
  this.setPendingFlexControllerEvent(key, eventArg);
11451
12161
  this.startFlexControllerEventDrain();
12162
+ return true;
11452
12163
  }
11453
12164
 
11454
12165
  private startFlexControllerEventDrain(): void {
@@ -12451,7 +13162,9 @@ export class OpenCodeController {
12451
13162
  transportConnectionArg: plugins.typedsocket.ISmartServeConnectionWrapper,
12452
13163
  requestArg: IReq_ControllerBrowserViewOpen['request'],
12453
13164
  signalArg: AbortSignal,
12454
- ): Promise<IReq_ControllerBrowserViewOpen['response']> {
13165
+ ): Promise<plugins.typedrequest.TReverseVirtualStreamDirections<
13166
+ IReq_ControllerBrowserViewOpen['response']
13167
+ >> {
12455
13168
  return this.runBrowserViewSlotOperation(peerArg.id, async (slotArg) => {
12456
13169
  const transportPeer = transportConnectionArg.peer;
12457
13170
  signalArg.throwIfAborted();
@@ -12508,17 +13221,17 @@ export class OpenCodeController {
12508
13221
  view = {
12509
13222
  id: viewId,
12510
13223
  peerId: peerArg.id,
13224
+ credentialId,
12511
13225
  transportConnection: transportConnectionArg,
12512
13226
  transportPeer,
12513
13227
  resource,
12514
13228
  handle,
12515
13229
  authority: handle.lease.getAuthority(),
12516
- activationState: 'inactive',
13230
+ lifecycle: 'inactive',
12517
13231
  activationAbortController: new AbortController(),
12518
- deliveryTail: Promise.resolve(),
12519
- pendingEventCount: 0,
12520
- pendingEventBytes: 0,
12521
- activeFrameStreams: new Set(),
13232
+ streamGeneration: 1,
13233
+ stateRevision: 0,
13234
+ recoveryAttempts: 0,
12522
13235
  closing: false,
12523
13236
  };
12524
13237
  slotArg.view = view;
@@ -12534,6 +13247,9 @@ export class OpenCodeController {
12534
13247
  );
12535
13248
  }
12536
13249
  this.browserViews.set(view.id, view);
13250
+ const transport = await this.createBrowserViewTransport(view, 1);
13251
+ this.beginBrowserViewTransportOpening(view, transport);
13252
+ signalArg.throwIfAborted();
12537
13253
  view.activationTimer = setTimeout(() => {
12538
13254
  this.scheduleBrowserViewClose(view!, new Error('Browser view activation timed out.'));
12539
13255
  }, browserViewActivationTimeoutMs);
@@ -12542,7 +13258,10 @@ export class OpenCodeController {
12542
13258
  viewId,
12543
13259
  resourceId: resource.id,
12544
13260
  attachmentRevision: resource.attachmentRevision,
12545
- state: this.toControllerBrowserViewState(handle.lease.getState()),
13261
+ streamGeneration: transport.generation,
13262
+ events: transport.events,
13263
+ operations: transport.operations,
13264
+ state: this.nextControllerBrowserViewState(view, handle.lease.getState()),
12546
13265
  };
12547
13266
  } catch (errorArg) {
12548
13267
  let cleanupError: unknown;
@@ -12568,43 +13287,53 @@ export class OpenCodeController {
12568
13287
 
12569
13288
  private async activateBrowserView(
12570
13289
  viewIdArg: string,
12571
- peerIdArg: string,
13290
+ peerArg: IControllerPeer,
13291
+ streamGenerationArg: number,
12572
13292
  signalArg: AbortSignal,
12573
13293
  ): Promise<void> {
12574
- await this.runBrowserViewSlotOperation(peerIdArg, async (slotArg) => {
13294
+ await this.runBrowserViewSlotOperation(peerArg.id, async (slotArg) => {
12575
13295
  const view = slotArg.view;
13296
+ const transport = view?.transport;
12576
13297
  if (
12577
13298
  !view
12578
13299
  || view.id !== viewIdArg
12579
13300
  || view.closing
13301
+ || view.transportPeer !== peerArg
13302
+ || view.credentialId !== peerArg.context.state[authenticatedPeerStateKey]
13303
+ || view.streamGeneration !== streamGenerationArg
13304
+ || !transport
13305
+ || transport.generation !== streamGenerationArg
12580
13306
  || this.browserViews.get(viewIdArg) !== view
12581
13307
  ) {
12582
13308
  throw new plugins.typedrequest.TypedResponseError('The browser view is unavailable.', {
12583
13309
  code: 'resource_unavailable',
12584
13310
  });
12585
13311
  }
12586
- if (view.activationState === 'active') return;
13312
+ await this.assertBrowserViewAuthority(
13313
+ view,
13314
+ streamGenerationArg,
13315
+ transport.identity,
13316
+ signalArg,
13317
+ );
13318
+ if (view.lifecycle === 'active' && transport.lifecycle === 'active') return;
12587
13319
  if (view.activationPromise) {
12588
13320
  await awaitWithAbortSignal(view.activationPromise, signalArg);
12589
13321
  return;
12590
13322
  }
12591
- view.activationState = 'activating';
12592
- const activation = view.handle.activate((eventArg) => {
12593
- if (view.closing || slotArg.view !== view) return;
12594
- this.queueBrowserViewEvent(view, eventArg);
12595
- });
13323
+ const recovering = view.lifecycle === 'recovering';
13324
+ if (!recovering && view.lifecycle !== 'inactive') {
13325
+ throw new plugins.typedrequest.TypedResponseError('The browser view cannot be activated.', {
13326
+ code: 'concurrent_change',
13327
+ });
13328
+ }
13329
+ if (!recovering) view.lifecycle = 'activating';
13330
+ const activation = this.activateBrowserViewTransport(view, transport, recovering, signalArg);
12596
13331
  view.activationPromise = activation;
12597
- const activationSignal = AbortSignal.any([
12598
- signalArg,
12599
- view.activationAbortController.signal,
12600
- ]);
12601
13332
  try {
12602
- await awaitWithAbortSignal(activation, activationSignal);
12603
- activationSignal.throwIfAborted();
13333
+ await activation;
12604
13334
  if (view.closing || slotArg.view !== view) {
12605
13335
  throw new Error('The browser view changed during activation.');
12606
13336
  }
12607
- view.activationState = 'active';
12608
13337
  if (view.activationTimer) clearTimeout(view.activationTimer);
12609
13338
  view.activationTimer = undefined;
12610
13339
  } catch (errorArg) {
@@ -12616,13 +13345,175 @@ export class OpenCodeController {
12616
13345
  });
12617
13346
  }
12618
13347
 
12619
- private async closeBrowserView(viewIdArg: string, peerIdArg: string): Promise<void> {
12620
- const slot = this.browserViewSlots.get(peerIdArg);
12621
- const view = slot?.view;
12622
- if (!slot || !view || view.id !== viewIdArg) return;
12623
- await this.requestBrowserViewCleanup(slot, view, new Error('Browser view closed.'));
12624
- }
12625
-
13348
+ private async activateBrowserViewTransport(
13349
+ viewArg: IControllerBrowserView,
13350
+ transportArg: IControllerBrowserViewTransport,
13351
+ recoveringArg: boolean,
13352
+ signalArg: AbortSignal,
13353
+ ): Promise<void> {
13354
+ const signals = [
13355
+ signalArg,
13356
+ viewArg.activationAbortController.signal,
13357
+ transportArg.abortController.signal,
13358
+ ];
13359
+ if (recoveringArg && viewArg.recoveryDeadlineAt !== undefined) {
13360
+ const remainingMs = viewArg.recoveryDeadlineAt - Date.now();
13361
+ if (remainingMs <= 0) throw new Error('Browser view recovery timed out.');
13362
+ signals.push(AbortSignal.timeout(remainingMs));
13363
+ }
13364
+ const activationSignal = AbortSignal.any(signals);
13365
+ await awaitWithAbortSignal(transportArg.openingTask, activationSignal);
13366
+ activationSignal.throwIfAborted();
13367
+ if (viewArg.transport !== transportArg || transportArg.lifecycle !== 'ready') {
13368
+ throw new Error('The browser view transport changed during activation.');
13369
+ }
13370
+ transportArg.lifecycle = 'active';
13371
+
13372
+ if (!recoveringArg) {
13373
+ await awaitWithAbortSignal(viewArg.handle.activate((eventArg) => {
13374
+ const currentTransport = viewArg.transport;
13375
+ if (viewArg.closing || !currentTransport) return;
13376
+ this.queueBrowserViewEvent(viewArg, currentTransport, eventArg);
13377
+ }), activationSignal);
13378
+ activationSignal.throwIfAborted();
13379
+ viewArg.lifecycle = 'active';
13380
+ this.ensureBrowserOperationDrain(viewArg, transportArg);
13381
+ return;
13382
+ }
13383
+
13384
+ const runtimeState = viewArg.handle.lease.getState();
13385
+ const activeTab = runtimeState.tabs.find((tabArg) => tabArg.id === runtimeState.activeTabId);
13386
+ if (!activeTab || !activeTab.active || activeTab.status !== 'open' || !activeTab.streaming) {
13387
+ throw new Error('The active browser tab cannot refresh its frame stream.');
13388
+ }
13389
+ const stateDelivery = this.queueBrowserServerPacket(viewArg, transportArg, {
13390
+ protocol: controllerBrowserTransportProtocol,
13391
+ streamGeneration: transportArg.generation,
13392
+ type: 'state',
13393
+ state: this.nextControllerBrowserViewState(viewArg, runtimeState),
13394
+ });
13395
+ if (!stateDelivery) throw new Error('The recovered browser state could not be queued.');
13396
+ await awaitWithAbortSignal(stateDelivery, activationSignal);
13397
+ viewArg.recoveryFrameGate = {
13398
+ tabId: activeTab.id,
13399
+ minimumGeneration: activeTab.generation + 1,
13400
+ viewportRevision: runtimeState.viewportRevision,
13401
+ admitted: false,
13402
+ };
13403
+ await viewArg.handle.refreshFrameStream(activationSignal);
13404
+ if (!viewArg.recoveryFrameGate.admitted) {
13405
+ throw new Error('BrowserRuntime did not deliver the recovered frame boundary.');
13406
+ }
13407
+ viewArg.recoveryFrameGate = undefined;
13408
+ viewArg.lifecycle = 'active';
13409
+ viewArg.recoveryDeadlineAt = undefined;
13410
+ if (viewArg.recoveryTimer) clearTimeout(viewArg.recoveryTimer);
13411
+ viewArg.recoveryTimer = undefined;
13412
+ this.ensureBrowserOperationDrain(viewArg, transportArg);
13413
+ }
13414
+
13415
+ private async recoverBrowserView(
13416
+ peerArg: IControllerPeer,
13417
+ transportConnectionArg: plugins.typedsocket.ISmartServeConnectionWrapper,
13418
+ requestArg: IReq_ControllerBrowserViewRecover['request'],
13419
+ signalArg: AbortSignal,
13420
+ ): Promise<plugins.typedrequest.TReverseVirtualStreamDirections<
13421
+ IReq_ControllerBrowserViewRecover['response']
13422
+ >> {
13423
+ return this.runBrowserViewSlotOperation(peerArg.id, async (slotArg) => {
13424
+ const view = slotArg.view;
13425
+ const oldTransport = view?.transport;
13426
+ if (
13427
+ !view
13428
+ || view.id !== requestArg.viewId
13429
+ || view.closing
13430
+ || view.transportConnection.peer !== transportConnectionArg.peer
13431
+ || view.transportPeer !== peerArg
13432
+ || view.credentialId !== peerArg.context.state[authenticatedPeerStateKey]
13433
+ || view.streamGeneration !== requestArg.expectedStreamGeneration
13434
+ || !oldTransport
13435
+ || oldTransport.generation !== requestArg.expectedStreamGeneration
13436
+ || (view.lifecycle !== 'active' && view.lifecycle !== 'recoverable')
13437
+ ) {
13438
+ throw new plugins.typedrequest.TypedResponseError('The browser view cannot be recovered.', {
13439
+ code: 'resource_unavailable',
13440
+ });
13441
+ }
13442
+ await this.assertBrowserViewAuthority(
13443
+ view,
13444
+ oldTransport.generation,
13445
+ oldTransport.identity,
13446
+ signalArg,
13447
+ );
13448
+ const now = Date.now();
13449
+ view.recoveryDeadlineAt ??= now + controllerBrowserTransportRecoveryGraceMs;
13450
+ if (
13451
+ now >= view.recoveryDeadlineAt
13452
+ || view.recoveryAttempts >= controllerBrowserTransportMaximumRecoveryAttempts
13453
+ ) {
13454
+ throw new plugins.typedrequest.TypedResponseError('Browser view recovery capacity was reached.', {
13455
+ code: 'operation_limit',
13456
+ });
13457
+ }
13458
+ view.recoveryAttempts += 1;
13459
+ view.lifecycle = 'recovering';
13460
+ this.armBrowserViewRecoveryDeadline(view);
13461
+ try {
13462
+ await this.retireBrowserViewTransport(
13463
+ view,
13464
+ oldTransport,
13465
+ Math.min(
13466
+ view.recoveryDeadlineAt,
13467
+ Date.now() + browserViewTransportCleanupTimeoutMs,
13468
+ ),
13469
+ );
13470
+ signalArg.throwIfAborted();
13471
+ if (Date.now() >= view.recoveryDeadlineAt) {
13472
+ throw new Error('Browser view recovery timed out.');
13473
+ }
13474
+ const streamGeneration = oldTransport.generation + 1;
13475
+ view.streamGeneration = streamGeneration;
13476
+ const transport = await this.createBrowserViewTransport(view, streamGeneration);
13477
+ this.beginBrowserViewTransportOpening(view, transport);
13478
+ signalArg.throwIfAborted();
13479
+ return {
13480
+ viewId: view.id,
13481
+ resourceId: view.resource.id,
13482
+ attachmentRevision: view.resource.attachmentRevision,
13483
+ streamGeneration,
13484
+ events: transport.events,
13485
+ operations: transport.operations,
13486
+ state: this.nextControllerBrowserViewState(view, view.handle.lease.getState()),
13487
+ };
13488
+ } catch (errorArg) {
13489
+ this.scheduleBrowserViewClose(view, errorArg);
13490
+ throw asTypedResponseError(errorArg);
13491
+ }
13492
+ });
13493
+ }
13494
+
13495
+ private async closeBrowserView(
13496
+ viewIdArg: string,
13497
+ peerArg: string | IControllerPeer,
13498
+ expectedStreamGenerationArg?: number,
13499
+ ): Promise<boolean> {
13500
+ const peerId = typeof peerArg === 'string' ? peerArg : peerArg.id;
13501
+ const slot = this.browserViewSlots.get(peerId);
13502
+ const view = slot?.view;
13503
+ if (
13504
+ !slot
13505
+ || !view
13506
+ || view.id !== viewIdArg
13507
+ || (typeof peerArg !== 'string' && view.transportPeer !== peerArg)
13508
+ || (
13509
+ expectedStreamGenerationArg !== undefined
13510
+ && view.streamGeneration !== expectedStreamGenerationArg
13511
+ )
13512
+ ) return false;
13513
+ await this.requestBrowserViewCleanup(slot, view, new Error('Browser view closed.'));
13514
+ return true;
13515
+ }
13516
+
12626
13517
  private async closeBrowserViewsForPeer(peerIdArg: string): Promise<void> {
12627
13518
  const slot = this.browserViewSlots.get(peerIdArg);
12628
13519
  const view = slot?.view;
@@ -12694,15 +13585,22 @@ export class OpenCodeController {
12694
13585
  private markBrowserViewClosing(viewArg: IControllerBrowserView, reasonArg: unknown): void {
12695
13586
  if (!viewArg.closing) {
12696
13587
  viewArg.closing = true;
13588
+ viewArg.lifecycle = 'closing';
12697
13589
  if (this.browserViews.get(viewArg.id) === viewArg) this.browserViews.delete(viewArg.id);
12698
13590
  if (viewArg.activationTimer) clearTimeout(viewArg.activationTimer);
12699
13591
  viewArg.activationTimer = undefined;
13592
+ if (viewArg.recoveryTimer) clearTimeout(viewArg.recoveryTimer);
13593
+ viewArg.recoveryTimer = undefined;
12700
13594
  if (!viewArg.activationAbortController.signal.aborted) {
12701
13595
  viewArg.activationAbortController.abort(reasonArg);
12702
13596
  }
12703
13597
  }
12704
- for (const stream of viewArg.activeFrameStreams) {
12705
- void stream.abort(reasonArg).catch(() => undefined);
13598
+ const transport = viewArg.transport;
13599
+ if (transport) {
13600
+ if (!transport.abortController.signal.aborted) transport.abortController.abort(reasonArg);
13601
+ transport.activeMutation?.abortController?.abort(reasonArg);
13602
+ void transport.events.abort(reasonArg).catch(() => undefined);
13603
+ void transport.operations.abort(reasonArg).catch(() => undefined);
12706
13604
  }
12707
13605
  }
12708
13606
 
@@ -12767,14 +13665,18 @@ export class OpenCodeController {
12767
13665
  this.markBrowserViewClosing(viewArg, reasonArg);
12768
13666
  if (slotArg.view !== viewArg) return;
12769
13667
  const errors: unknown[] = [];
12770
- const streamResults = await Promise.allSettled(
12771
- [...viewArg.activeFrameStreams].map((stream) => stream.abort(reasonArg)),
12772
- );
12773
- errors.push(...streamResults
12774
- .filter((result): result is PromiseRejectedResult => result.status === 'rejected')
12775
- .map((result) => result.reason));
13668
+ const constructionCleanup = viewArg.transportConstructionCleanup;
13669
+ if (constructionCleanup && !constructionCleanup.settled) {
13670
+ errors.push(new Error('Browser stream pair construction cleanup remains pending.'));
13671
+ }
13672
+ if (viewArg.transport) {
13673
+ await this.retireBrowserViewTransport(
13674
+ viewArg,
13675
+ viewArg.transport,
13676
+ Date.now() + browserViewTransportCleanupTimeoutMs,
13677
+ ).catch((errorArg) => errors.push(errorArg));
13678
+ }
12776
13679
  await viewArg.handle.close().catch((errorArg) => errors.push(errorArg));
12777
- await viewArg.deliveryTail.catch((errorArg) => errors.push(errorArg));
12778
13680
  if (errors.length === 1) throw errors[0];
12779
13681
  if (errors.length > 1) {
12780
13682
  throw new AggregateError(errors, 'Browser view cleanup is incomplete.');
@@ -12790,6 +13692,13 @@ export class OpenCodeController {
12790
13692
  reasonArg: unknown,
12791
13693
  ): void {
12792
13694
  if (slotArg.view !== viewArg || viewArg.cleanupRetryTimer) return;
13695
+ if (
13696
+ viewArg.transportConstructionCleanup
13697
+ || (
13698
+ viewArg.transport?.retirementSettlement
13699
+ && !viewArg.transport.retirementSettled
13700
+ )
13701
+ ) return;
12793
13702
  const timer = setTimeout(() => {
12794
13703
  if (viewArg.cleanupRetryTimer === timer) viewArg.cleanupRetryTimer = undefined;
12795
13704
  if (slotArg.view !== viewArg) return;
@@ -12801,250 +13710,932 @@ export class OpenCodeController {
12801
13710
  timer.unref?.();
12802
13711
  }
12803
13712
 
12804
- private async operateBrowserView(
12805
- peerIdArg: string,
12806
- viewIdArg: string,
12807
- operationArg: IReq_ControllerBrowserViewOperate['request']['operation'],
12808
- signalArg: AbortSignal,
12809
- ): Promise<IReq_ControllerBrowserViewOperate['response']> {
12810
- const view = this.browserViews.get(viewIdArg);
12811
- if (!view || view.peerId !== peerIdArg || view.closing) {
12812
- throw new plugins.typedrequest.TypedResponseError('The browser view is unavailable.', {
12813
- code: 'resource_unavailable',
13713
+ private queueBrowserViewEvent(
13714
+ viewArg: IControllerBrowserView,
13715
+ transportArg: IControllerBrowserViewTransport,
13716
+ eventArg: plugins.browserRuntime.TBrowserRuntimeEvent,
13717
+ ): void {
13718
+ try {
13719
+ if (viewArg.lifecycle === 'failing' || viewArg.lifecycle === 'closing') return;
13720
+ if (eventArg.type === 'state') {
13721
+ if (viewArg.lifecycle === 'recoverable') return;
13722
+ this.queueBrowserServerPacket(viewArg, transportArg, {
13723
+ protocol: controllerBrowserTransportProtocol,
13724
+ streamGeneration: transportArg.generation,
13725
+ type: 'state',
13726
+ state: this.nextControllerBrowserViewState(viewArg, eventArg.state),
13727
+ });
13728
+ return;
13729
+ }
13730
+ if (eventArg.type === 'error') {
13731
+ const delivery = this.queueBrowserServerPacket(viewArg, transportArg, {
13732
+ protocol: controllerBrowserTransportProtocol,
13733
+ streamGeneration: transportArg.generation,
13734
+ type: 'error',
13735
+ error: { ...eventArg.error, message: eventArg.error.code },
13736
+ });
13737
+ if (eventArg.error.fatal) {
13738
+ const reason = new Error(eventArg.error.code);
13739
+ viewArg.lifecycle = 'failing';
13740
+ this.terminalizeBrowserTransportOperations(transportArg, reason);
13741
+ if (!delivery) {
13742
+ this.scheduleBrowserViewClose(viewArg, reason);
13743
+ } else {
13744
+ void awaitWithAbortSignal(
13745
+ delivery,
13746
+ AbortSignal.timeout(browserViewTransportCleanupTimeoutMs),
13747
+ ).then(
13748
+ () => this.scheduleBrowserViewClose(viewArg, reason),
13749
+ () => this.scheduleBrowserViewClose(viewArg, reason),
13750
+ );
13751
+ }
13752
+ }
13753
+ return;
13754
+ }
13755
+ if (viewArg.lifecycle === 'recoverable') return;
13756
+ const gate = viewArg.recoveryFrameGate;
13757
+ if (viewArg.lifecycle === 'recovering') {
13758
+ if (
13759
+ !gate
13760
+ || eventArg.frame.tabId !== gate.tabId
13761
+ || eventArg.frame.generation < gate.minimumGeneration
13762
+ || eventArg.frame.viewportRevision !== gate.viewportRevision
13763
+ ) return;
13764
+ }
13765
+ const { data, ...frame } = eventArg.frame;
13766
+ if (data.byteLength > controllerBrowserTransportMaximumFrameBytes) {
13767
+ throw new Error('The browser frame exceeds the transport byte limit.');
13768
+ }
13769
+ const copiedData = Uint8Array.from(data);
13770
+ const metadata: IControllerBrowserFrameMetadata = {
13771
+ ...frame,
13772
+ dataByteLength: copiedData.byteLength,
13773
+ };
13774
+ this.queueBrowserServerPacket(viewArg, transportArg, {
13775
+ protocol: controllerBrowserTransportProtocol,
13776
+ streamGeneration: transportArg.generation,
13777
+ type: 'frame',
13778
+ frame: metadata,
13779
+ }, copiedData);
13780
+ if (gate) gate.admitted = true;
13781
+ } catch (errorArg) {
13782
+ this.scheduleBrowserViewClose(viewArg, errorArg);
13783
+ throw errorArg;
13784
+ }
13785
+ }
13786
+
13787
+ private async createBrowserViewTransport(
13788
+ viewArg: IControllerBrowserView,
13789
+ streamGenerationArg: number,
13790
+ ): Promise<IControllerBrowserViewTransport> {
13791
+ const typedSocket = this.typedServer?.typedsocket;
13792
+ if (!typedSocket) throw new Error('The controller transport is unavailable.');
13793
+ const transportIdentity = Symbol('controller.browser.transport');
13794
+ if (viewArg.pendingTransportIdentity || viewArg.transportConstructionCleanup) {
13795
+ throw new Error('A browser stream pair is already being constructed.');
13796
+ }
13797
+ viewArg.pendingTransportIdentity = transportIdentity;
13798
+ const createAuthorization = (
13799
+ creatorDirectionArg: plugins.typedrequestInterfaces.TVirtualStreamDirection,
13800
+ contentTypeArg: string,
13801
+ ): IControllerBrowserStreamAuthorization => Object.freeze({
13802
+ marker: browserStreamAuthorizationMarker,
13803
+ view: viewArg,
13804
+ authority: viewArg.authority,
13805
+ transportPeer: viewArg.transportPeer,
13806
+ transportIdentity,
13807
+ streamGeneration: streamGenerationArg,
13808
+ creatorDirection: creatorDirectionArg,
13809
+ contentType: contentTypeArg,
13810
+ });
13811
+ try {
13812
+ const events = typedSocket.createVirtualStream({
13813
+ target: viewArg.transportConnection,
13814
+ creatorDirection: 'send',
13815
+ contentType: controllerBrowserTransportEventContentType,
13816
+ authorization: createAuthorization('send', controllerBrowserTransportEventContentType),
12814
13817
  });
13818
+ let operations: plugins.typedrequestInterfaces.TVirtualStream<'receive'>;
13819
+ try {
13820
+ operations = typedSocket.createVirtualStream({
13821
+ target: viewArg.transportConnection,
13822
+ creatorDirection: 'receive',
13823
+ contentType: controllerBrowserTransportOperationContentType,
13824
+ authorization: createAuthorization('receive', controllerBrowserTransportOperationContentType),
13825
+ });
13826
+ } catch (errorArg) {
13827
+ let cleanup!: IControllerBrowserTransportConstructionCleanup;
13828
+ const settlement = Promise.allSettled([
13829
+ Promise.resolve().then(() => events.abort(errorArg)),
13830
+ events.opened,
13831
+ events.completion,
13832
+ events.closed,
13833
+ ]).then(() => {
13834
+ cleanup.settled = true;
13835
+ const slot = this.browserViewSlots.get(viewArg.peerId);
13836
+ if (
13837
+ !cleanup.deadlineExpired
13838
+ || !viewArg.closing
13839
+ || slot?.view !== viewArg
13840
+ ) {
13841
+ if (viewArg.transportConstructionCleanup === cleanup) {
13842
+ viewArg.transportConstructionCleanup = undefined;
13843
+ }
13844
+ return;
13845
+ }
13846
+ const activeCleanupTask = viewArg.cleanupTask;
13847
+ void (activeCleanupTask
13848
+ ? activeCleanupTask.catch(() => undefined)
13849
+ : Promise.resolve()
13850
+ ).then(() => {
13851
+ const currentSlot = this.browserViewSlots.get(viewArg.peerId);
13852
+ if (
13853
+ viewArg.transportConstructionCleanup !== cleanup
13854
+ || !viewArg.closing
13855
+ || currentSlot?.view !== viewArg
13856
+ ) return;
13857
+ viewArg.transportConstructionCleanup = undefined;
13858
+ void this.requestBrowserViewCleanup(currentSlot, viewArg, cleanup.reason).catch(
13859
+ (cleanupErrorArg) => {
13860
+ console.error(`Browser view ${viewArg.id} late construction cleanup failed.`, cleanupErrorArg);
13861
+ },
13862
+ );
13863
+ });
13864
+ });
13865
+ cleanup = {
13866
+ settlement,
13867
+ reason: errorArg,
13868
+ settled: false,
13869
+ deadlineExpired: false,
13870
+ };
13871
+ viewArg.transportConstructionCleanup = cleanup;
13872
+ try {
13873
+ await this.waitForBrowserTransportSettlement(
13874
+ settlement,
13875
+ Date.now() + browserViewTransportCleanupTimeoutMs,
13876
+ );
13877
+ } catch (cleanupErrorArg) {
13878
+ cleanup.deadlineExpired = true;
13879
+ throw new AggregateError(
13880
+ [errorArg, cleanupErrorArg],
13881
+ 'Browser stream pair construction cleanup is incomplete.',
13882
+ );
13883
+ }
13884
+ throw errorArg;
13885
+ }
13886
+ const transport: IControllerBrowserViewTransport = {
13887
+ identity: transportIdentity,
13888
+ generation: streamGenerationArg,
13889
+ lifecycle: 'opening',
13890
+ events,
13891
+ operations,
13892
+ abortController: new AbortController(),
13893
+ openingTask: Promise.resolve(),
13894
+ deliveryTail: Promise.resolve(),
13895
+ pendingEventCount: 0,
13896
+ pendingEventBytes: 0,
13897
+ operationQueue: [],
13898
+ queuedOperationBytes: 0,
13899
+ highestOperationId: 0,
13900
+ cancellableOperationIds: new Set(),
13901
+ activeAcknowledgements: new Map(),
13902
+ };
13903
+ viewArg.transport = transport;
13904
+ return transport;
13905
+ } finally {
13906
+ if (viewArg.pendingTransportIdentity === transportIdentity) {
13907
+ viewArg.pendingTransportIdentity = undefined;
13908
+ }
12815
13909
  }
12816
- const current = await awaitWithAbortSignal(
12817
- this.requireAuthStore().getResource(view.resource.projectId, view.resource.id),
12818
- signalArg,
12819
- );
13910
+ }
13911
+
13912
+ private beginBrowserViewTransportOpening(
13913
+ viewArg: IControllerBrowserView,
13914
+ transportArg: IControllerBrowserViewTransport,
13915
+ ): void {
13916
+ if (viewArg.transport !== transportArg) {
13917
+ throw new Error('The browser stream pair cannot open before publication.');
13918
+ }
13919
+ const openingTask = this.openBrowserViewTransport(viewArg, transportArg);
13920
+ transportArg.openingTask = openingTask;
13921
+ void openingTask.catch((errorArg) => {
13922
+ this.noteBrowserTransportFault(viewArg, transportArg, errorArg, true);
13923
+ });
13924
+ }
13925
+
13926
+ private async openBrowserViewTransport(
13927
+ viewArg: IControllerBrowserView,
13928
+ transportArg: IControllerBrowserViewTransport,
13929
+ ): Promise<void> {
13930
+ let timeout: ReturnType<typeof setTimeout> | undefined;
13931
+ try {
13932
+ await Promise.race([
13933
+ Promise.all([transportArg.events.opened, transportArg.operations.opened]),
13934
+ new Promise<never>((_resolve, reject) => {
13935
+ timeout = setTimeout(() => reject(new Error('Browser stream pair opening timed out.')),
13936
+ controllerBrowserTransportPairOpenTimeoutMs);
13937
+ timeout.unref?.();
13938
+ }),
13939
+ ]);
13940
+ if (
13941
+ viewArg.transport !== transportArg
13942
+ || viewArg.closing
13943
+ || viewArg.streamGeneration !== transportArg.generation
13944
+ || transportArg.lifecycle !== 'opening'
13945
+ ) throw new Error('The browser stream pair became stale while opening.');
13946
+ transportArg.lifecycle = 'ready';
13947
+ const receiveTask = this.receiveBrowserOperations(viewArg, transportArg);
13948
+ transportArg.receiveTask = receiveTask;
13949
+ void receiveTask.catch((errorArg) => {
13950
+ this.noteBrowserTransportFault(viewArg, transportArg, errorArg, true);
13951
+ });
13952
+ } finally {
13953
+ if (timeout) clearTimeout(timeout);
13954
+ }
13955
+ }
13956
+
13957
+ private async receiveBrowserOperations(
13958
+ viewArg: IControllerBrowserView,
13959
+ transportArg: IControllerBrowserViewTransport,
13960
+ ): Promise<void> {
13961
+ while (transportArg.lifecycle !== 'retiring' && transportArg.lifecycle !== 'retired') {
13962
+ const chunk = await transportArg.operations.receive();
13963
+ if (chunk === undefined) {
13964
+ if (transportArg.abortController.signal.aborted) return;
13965
+ await transportArg.operations.accept();
13966
+ throw new Error('The browser operation stream closed unexpectedly.');
13967
+ }
13968
+ let packet: TControllerBrowserClientPacket;
13969
+ try {
13970
+ packet = assertControllerBrowserClientPacket(
13971
+ decodeControllerBrowserTransportPacket(chunk),
13972
+ );
13973
+ } catch (errorArg) {
13974
+ this.failBrowserTransportProtocol(viewArg, transportArg, errorArg);
13975
+ return;
13976
+ }
13977
+ if (packet.streamGeneration !== transportArg.generation) {
13978
+ this.failBrowserTransportProtocol(
13979
+ viewArg,
13980
+ transportArg,
13981
+ new Error('The browser operation belongs to a stale stream generation.'),
13982
+ );
13983
+ return;
13984
+ }
13985
+ this.admitBrowserClientPacket(viewArg, transportArg, packet, chunk.byteLength);
13986
+ }
13987
+ }
13988
+
13989
+ private admitBrowserClientPacket(
13990
+ viewArg: IControllerBrowserView,
13991
+ transportArg: IControllerBrowserViewTransport,
13992
+ packetArg: TControllerBrowserClientPacket,
13993
+ byteLengthArg: number,
13994
+ ): void {
13995
+ if (packetArg.type === 'cancel') {
13996
+ if (packetArg.operationId > transportArg.highestOperationId) {
13997
+ this.failBrowserTransportProtocol(viewArg, transportArg, new Error('cancel targeted a future operation.'));
13998
+ return;
13999
+ }
14000
+ if (transportArg.cancellableOperationIds.has(packetArg.operationId)) {
14001
+ this.cancelBrowserOperation(viewArg, transportArg, packetArg.operationId);
14002
+ }
14003
+ return;
14004
+ }
14005
+ const admitted = this.consumeBrowserRealtimeAdmission(viewArg.peerId);
14006
+ if (!admitted) {
14007
+ if (packetArg.operationId <= transportArg.highestOperationId) {
14008
+ this.failBrowserTransportProtocol(viewArg, transportArg, new Error('operationId was reused.'));
14009
+ return;
14010
+ }
14011
+ transportArg.highestOperationId = packetArg.operationId;
14012
+ this.queueBrowserOperationResult(viewArg, transportArg, packetArg.operationId, {
14013
+ status: 'failed',
14014
+ error: { code: 'rate_limited', message: 'The browser input rate limit was reached.' },
14015
+ });
14016
+ return;
14017
+ }
14018
+ if (packetArg.operationId <= transportArg.highestOperationId) {
14019
+ this.failBrowserTransportProtocol(viewArg, transportArg, new Error('operationId was reused.'));
14020
+ return;
14021
+ }
14022
+ transportArg.highestOperationId = packetArg.operationId;
14023
+ const queued: IControllerBrowserQueuedOperation = {
14024
+ operationId: packetArg.operationId,
14025
+ operation: packetArg.operation,
14026
+ byteLength: byteLengthArg,
14027
+ cancelled: false,
14028
+ terminal: false,
14029
+ };
12820
14030
  if (
12821
- !current
12822
- || current.lifecycle !== 'active'
12823
- || current.kind !== 'browser'
12824
- || current.pendingAttachment
12825
- || current.attachmentAuthorityId !== view.resource.attachmentAuthorityId
12826
- || current.attachmentRevision !== view.resource.attachmentRevision
14031
+ transportArg.operationQueue.length >= controllerBrowserTransportMaximumPendingOperations
14032
+ || transportArg.queuedOperationBytes + byteLengthArg
14033
+ > controllerBrowserTransportMaximumQueuedOperationBytes
12827
14034
  ) {
12828
- await this.closeBrowserView(view.id, peerIdArg);
12829
- throw new plugins.typedrequest.TypedResponseError('The browser resource changed.', {
12830
- code: 'resource_unavailable',
14035
+ this.settleBrowserOperation(viewArg, transportArg, queued, {
14036
+ status: 'failed',
14037
+ error: { code: 'operation_limit', message: 'The browser operation queue is full.' },
12831
14038
  });
14039
+ return;
12832
14040
  }
12833
- const lease = view.handle.lease;
12834
- let accepted = true;
12835
- try {
12836
- switch (operationArg.type) {
12837
- case 'acknowledgeFrame':
12838
- signalArg.throwIfAborted();
12839
- accepted = await lease.acknowledgeFrame(operationArg.acknowledgement);
12840
- signalArg.throwIfAborted();
12841
- break;
12842
- case 'setViewport':
12843
- await lease.setViewport(operationArg.viewport, { signal: signalArg });
12844
- break;
12845
- case 'dispatchMouse':
12846
- await lease.dispatchMouse(operationArg.input, { signal: signalArg });
12847
- break;
12848
- case 'dispatchWheel':
12849
- await lease.dispatchWheel(operationArg.input, { signal: signalArg });
12850
- break;
12851
- case 'dispatchKey':
12852
- await lease.dispatchKey(operationArg.input, { signal: signalArg });
12853
- break;
12854
- case 'insertText':
12855
- await lease.insertText(operationArg.input, { signal: signalArg });
12856
- break;
12857
- case 'navigate':
12858
- await lease.executeHumanAction({
12859
- action: 'navigate',
12860
- url: operationArg.url,
12861
- ...(operationArg.tabId === undefined ? {} : { tabId: operationArg.tabId }),
12862
- ...(operationArg.timeoutMs === undefined ? {} : { timeoutMs: operationArg.timeoutMs }),
12863
- }, { signal: signalArg });
12864
- break;
12865
- case 'createTab':
12866
- await lease.createTab({
12867
- ...(operationArg.url === undefined ? {} : { url: operationArg.url }),
12868
- ...(operationArg.activate === undefined ? {} : { activate: operationArg.activate }),
12869
- }, { signal: signalArg });
12870
- break;
12871
- case 'activateTab':
12872
- await lease.activateTab(operationArg.tabId, { signal: signalArg });
12873
- break;
12874
- case 'closeTab':
12875
- await lease.closeTab(operationArg.tabId, { signal: signalArg });
12876
- break;
12877
- case 'back':
12878
- await lease.back(
12879
- operationArg.tabId === undefined ? {} : { tabId: operationArg.tabId },
12880
- { signal: signalArg },
12881
- );
12882
- break;
12883
- case 'forward':
12884
- await lease.forward(
12885
- operationArg.tabId === undefined ? {} : { tabId: operationArg.tabId },
12886
- { signal: signalArg },
12887
- );
12888
- break;
12889
- case 'reload':
12890
- await lease.reload(
12891
- operationArg.tabId === undefined ? {} : { tabId: operationArg.tabId },
12892
- { signal: signalArg },
12893
- );
12894
- break;
14041
+ transportArg.cancellableOperationIds.add(queued.operationId);
14042
+ const tail = transportArg.operationQueue.at(-1);
14043
+ if (tail && this.browserOperationsCanCoalesce(tail.operation, queued.operation)) {
14044
+ transportArg.operationQueue.pop();
14045
+ transportArg.queuedOperationBytes -= tail.byteLength;
14046
+ this.settleBrowserOperation(viewArg, transportArg, tail, { status: 'superseded' });
14047
+ }
14048
+ transportArg.operationQueue.push(queued);
14049
+ transportArg.queuedOperationBytes += byteLengthArg;
14050
+ if (viewArg.lifecycle === 'active' && transportArg.lifecycle === 'active') {
14051
+ if (queued.operation.type === 'acknowledgeFrame') {
14052
+ const index = transportArg.operationQueue.indexOf(queued);
14053
+ if (index >= 0) {
14054
+ transportArg.operationQueue.splice(index, 1);
14055
+ transportArg.queuedOperationBytes -= queued.byteLength;
14056
+ this.startBrowserAcknowledgement(viewArg, transportArg, queued);
14057
+ }
14058
+ } else {
14059
+ this.ensureBrowserOperationDrain(viewArg, transportArg);
12895
14060
  }
12896
- } catch (errorArg) {
12897
- throw asTypedResponseError(errorArg);
12898
14061
  }
12899
- return { accepted, state: this.toControllerBrowserViewState(lease.getState()) };
12900
14062
  }
12901
14063
 
12902
- private queueBrowserViewEvent(
14064
+ private consumeBrowserRealtimeAdmission(peerIdArg: string): boolean {
14065
+ let peerBucket = this.browserViewRealtimeBucketsByPeer.get(peerIdArg);
14066
+ if (!peerBucket) {
14067
+ peerBucket = new ControllerBrowserTokenBucket(
14068
+ browserViewRealtimePeerRate,
14069
+ browserViewRealtimePeerBurst,
14070
+ );
14071
+ this.browserViewRealtimeBucketsByPeer.set(peerIdArg, peerBucket);
14072
+ }
14073
+ const now = performance.now();
14074
+ return peerBucket.consume(now) && this.browserViewRealtimeGlobalBucket.consume(now);
14075
+ }
14076
+
14077
+ private browserOperationsCanCoalesce(
14078
+ existingArg: TControllerBrowserViewOperation,
14079
+ candidateArg: TControllerBrowserViewOperation,
14080
+ ): boolean {
14081
+ if (existingArg.type === 'setViewport' && candidateArg.type === 'setViewport') return true;
14082
+ return existingArg.type === 'dispatchMouse'
14083
+ && candidateArg.type === 'dispatchMouse'
14084
+ && existingArg.input.type === 'move'
14085
+ && candidateArg.input.type === 'move'
14086
+ && existingArg.input.tabId === candidateArg.input.tabId
14087
+ && existingArg.input.generation === candidateArg.input.generation
14088
+ && existingArg.input.viewportRevision === candidateArg.input.viewportRevision;
14089
+ }
14090
+
14091
+ private cancelBrowserOperation(
12903
14092
  viewArg: IControllerBrowserView,
12904
- eventArg: plugins.browserRuntime.TBrowserRuntimeEvent,
14093
+ transportArg: IControllerBrowserViewTransport,
14094
+ operationIdArg: number,
14095
+ ): void {
14096
+ if (!transportArg.cancellableOperationIds.delete(operationIdArg)) return;
14097
+ const queuedIndex = transportArg.operationQueue.findIndex(
14098
+ (candidateArg) => candidateArg.operationId === operationIdArg,
14099
+ );
14100
+ if (queuedIndex >= 0) {
14101
+ const [queued] = transportArg.operationQueue.splice(queuedIndex, 1);
14102
+ if (!queued) return;
14103
+ transportArg.queuedOperationBytes -= queued.byteLength;
14104
+ queued.cancelled = true;
14105
+ this.settleBrowserOperation(viewArg, transportArg, queued, { status: 'cancelled' });
14106
+ return;
14107
+ }
14108
+ const active = transportArg.activeMutation;
14109
+ if (active?.operationId === operationIdArg && !active.terminal) {
14110
+ active.cancelled = true;
14111
+ active.abortController?.abort(new Error('The browser operation was cancelled.'));
14112
+ return;
14113
+ }
14114
+ const acknowledgement = transportArg.activeAcknowledgements.get(operationIdArg);
14115
+ if (acknowledgement && !acknowledgement.terminal) {
14116
+ acknowledgement.terminal = true;
14117
+ this.queueBrowserOperationResult(viewArg, transportArg, operationIdArg, {
14118
+ status: 'cancelled',
14119
+ });
14120
+ }
14121
+ }
14122
+
14123
+ private ensureBrowserOperationDrain(
14124
+ viewArg: IControllerBrowserView,
14125
+ transportArg: IControllerBrowserViewTransport,
12905
14126
  ): void {
12906
- const eventBytes = eventArg.type === 'frame' ? eventArg.frame.data.byteLength : 0;
12907
14127
  if (
12908
- viewArg.pendingEventCount >= maxPendingBrowserViewEvents
12909
- || eventBytes > plugins.typedsocket.VIRTUAL_STREAM_MAX_LOGICAL_CHUNK_BYTES
12910
- || viewArg.pendingEventBytes + eventBytes > maxPendingBrowserViewEventBytes
14128
+ transportArg.drainTask
14129
+ || viewArg.lifecycle !== 'active'
14130
+ || transportArg.lifecycle !== 'active'
14131
+ ) return;
14132
+ const drainTask = this.drainBrowserOperations(viewArg, transportArg).finally(() => {
14133
+ if (transportArg.drainTask === drainTask) transportArg.drainTask = undefined;
14134
+ if (
14135
+ viewArg.lifecycle === 'active'
14136
+ && transportArg.lifecycle === 'active'
14137
+ && transportArg.operationQueue.length > 0
14138
+ ) this.ensureBrowserOperationDrain(viewArg, transportArg);
14139
+ });
14140
+ transportArg.drainTask = drainTask;
14141
+ void drainTask.catch((errorArg) => {
14142
+ this.noteBrowserTransportFault(viewArg, transportArg, errorArg, false);
14143
+ });
14144
+ }
14145
+
14146
+ private async drainBrowserOperations(
14147
+ viewArg: IControllerBrowserView,
14148
+ transportArg: IControllerBrowserViewTransport,
14149
+ ): Promise<void> {
14150
+ while (
14151
+ viewArg.lifecycle === 'active'
14152
+ && transportArg.lifecycle === 'active'
14153
+ && viewArg.transport === transportArg
12911
14154
  ) {
12912
- this.scheduleBrowserViewClose(viewArg, new Error('The browser view delivery queue is full.'));
14155
+ const queued = transportArg.operationQueue.shift();
14156
+ if (!queued) return;
14157
+ transportArg.queuedOperationBytes -= queued.byteLength;
14158
+ if (queued.terminal) continue;
14159
+ if (queued.operation.type === 'acknowledgeFrame') {
14160
+ this.startBrowserAcknowledgement(viewArg, transportArg, queued);
14161
+ continue;
14162
+ }
14163
+ transportArg.activeMutation = queued;
14164
+ queued.abortController = new AbortController();
14165
+ const peerAbortController = viewArg.transportPeer.context.state[
14166
+ operationAbortControllerPeerStateKey
14167
+ ];
14168
+ const operationSignal = AbortSignal.any([
14169
+ queued.abortController.signal,
14170
+ viewArg.activationAbortController.signal,
14171
+ transportArg.abortController.signal,
14172
+ this.operationAbortController.signal,
14173
+ ...(peerAbortController instanceof AbortController ? [peerAbortController.signal] : []),
14174
+ ]);
14175
+ try {
14176
+ const state = await this.runBrowserMutationOperation(
14177
+ viewArg,
14178
+ transportArg,
14179
+ queued.operation,
14180
+ operationSignal,
14181
+ );
14182
+ this.settleBrowserOperation(viewArg, transportArg, queued, {
14183
+ status: 'succeeded',
14184
+ ...(state === undefined ? {} : { state }),
14185
+ });
14186
+ } catch (errorArg) {
14187
+ this.settleBrowserOperation(
14188
+ viewArg,
14189
+ transportArg,
14190
+ queued,
14191
+ queued.cancelled || operationSignal.aborted
14192
+ ? { status: 'cancelled' }
14193
+ : { status: 'failed', error: this.browserOperationError(errorArg) },
14194
+ );
14195
+ } finally {
14196
+ if (transportArg.activeMutation === queued) transportArg.activeMutation = undefined;
14197
+ }
14198
+ }
14199
+ }
14200
+
14201
+ private async runBrowserMutationOperation(
14202
+ viewArg: IControllerBrowserView,
14203
+ transportArg: IControllerBrowserViewTransport,
14204
+ operationArg: Exclude<TControllerBrowserViewOperation, { type: 'acknowledgeFrame' }>,
14205
+ signalArg: AbortSignal,
14206
+ ): Promise<IControllerBrowserViewState | undefined> {
14207
+ const execute = async (operationSignalArg: AbortSignal) => {
14208
+ await this.assertBrowserViewAuthority(
14209
+ viewArg,
14210
+ transportArg.generation,
14211
+ transportArg.identity,
14212
+ operationSignalArg,
14213
+ );
14214
+ await this.executeBrowserMutation(viewArg, operationArg, operationSignalArg);
14215
+ };
14216
+ if (this.isBrowserSemanticOperation(operationArg)) {
14217
+ await this.runOperation(viewArg.transportPeer, execute, signalArg);
14218
+ return this.nextControllerBrowserViewState(viewArg, viewArg.handle.lease.getState());
14219
+ }
14220
+ if (this.lifecycleState !== 'ready' || this.stopRequested) {
14221
+ throw new Error('The controller is not ready to accept browser input.');
14222
+ }
14223
+ await execute(signalArg);
14224
+ return undefined;
14225
+ }
14226
+
14227
+ private isBrowserSemanticOperation(operationArg: TControllerBrowserViewOperation): boolean {
14228
+ return operationArg.type === 'navigate'
14229
+ || operationArg.type === 'createTab'
14230
+ || operationArg.type === 'activateTab'
14231
+ || operationArg.type === 'closeTab'
14232
+ || operationArg.type === 'back'
14233
+ || operationArg.type === 'forward'
14234
+ || operationArg.type === 'reload';
14235
+ }
14236
+
14237
+ private async executeBrowserMutation(
14238
+ viewArg: IControllerBrowserView,
14239
+ operationArg: Exclude<TControllerBrowserViewOperation, { type: 'acknowledgeFrame' }>,
14240
+ signalArg: AbortSignal,
14241
+ ): Promise<void> {
14242
+ const lease = viewArg.handle.lease;
14243
+ switch (operationArg.type) {
14244
+ case 'setViewport':
14245
+ await lease.setViewport(operationArg.viewport, { signal: signalArg });
14246
+ return;
14247
+ case 'dispatchMouse':
14248
+ await lease.dispatchMouse(operationArg.input, { signal: signalArg });
14249
+ return;
14250
+ case 'dispatchWheel':
14251
+ await lease.dispatchWheel(operationArg.input, { signal: signalArg });
14252
+ return;
14253
+ case 'dispatchKey':
14254
+ await lease.dispatchKey(operationArg.input, { signal: signalArg });
14255
+ return;
14256
+ case 'insertText':
14257
+ await lease.insertText(operationArg.input, { signal: signalArg });
14258
+ return;
14259
+ case 'navigate':
14260
+ await lease.executeHumanAction({
14261
+ action: 'navigate',
14262
+ url: operationArg.url,
14263
+ ...(operationArg.tabId === undefined ? {} : { tabId: operationArg.tabId }),
14264
+ ...(operationArg.timeoutMs === undefined ? {} : { timeoutMs: operationArg.timeoutMs }),
14265
+ }, { signal: signalArg });
14266
+ return;
14267
+ case 'createTab':
14268
+ await lease.createTab({
14269
+ ...(operationArg.url === undefined ? {} : { url: operationArg.url }),
14270
+ ...(operationArg.activate === undefined ? {} : { activate: operationArg.activate }),
14271
+ }, { signal: signalArg });
14272
+ return;
14273
+ case 'activateTab':
14274
+ await lease.activateTab(operationArg.tabId, { signal: signalArg });
14275
+ return;
14276
+ case 'closeTab':
14277
+ await lease.closeTab(operationArg.tabId, { signal: signalArg });
14278
+ return;
14279
+ case 'back':
14280
+ await lease.back(
14281
+ operationArg.tabId === undefined ? {} : { tabId: operationArg.tabId },
14282
+ { signal: signalArg },
14283
+ );
14284
+ return;
14285
+ case 'forward':
14286
+ await lease.forward(
14287
+ operationArg.tabId === undefined ? {} : { tabId: operationArg.tabId },
14288
+ { signal: signalArg },
14289
+ );
14290
+ return;
14291
+ case 'reload':
14292
+ await lease.reload(
14293
+ operationArg.tabId === undefined ? {} : { tabId: operationArg.tabId },
14294
+ { signal: signalArg },
14295
+ );
14296
+ }
14297
+ }
14298
+
14299
+ private startBrowserAcknowledgement(
14300
+ viewArg: IControllerBrowserView,
14301
+ transportArg: IControllerBrowserViewTransport,
14302
+ queuedArg: IControllerBrowserQueuedOperation,
14303
+ ): void {
14304
+ const operationRequest = queuedArg.operation;
14305
+ if (operationRequest.type !== 'acknowledgeFrame') return;
14306
+ if (
14307
+ transportArg.activeAcknowledgements.size
14308
+ >= controllerBrowserTransportMaximumActiveAcknowledgements
14309
+ ) {
14310
+ this.settleBrowserOperation(viewArg, transportArg, queuedArg, {
14311
+ status: 'failed',
14312
+ error: { code: 'operation_limit', message: 'Frame acknowledgement capacity was reached.' },
14313
+ });
12913
14314
  return;
12914
14315
  }
12915
- const queuedEvent: plugins.browserRuntime.TBrowserRuntimeEvent = eventArg.type === 'frame'
12916
- ? { type: 'frame', frame: { ...eventArg.frame, data: Uint8Array.from(eventArg.frame.data) } }
12917
- : eventArg;
12918
- viewArg.pendingEventCount += 1;
12919
- viewArg.pendingEventBytes += eventBytes;
12920
- const delivery = viewArg.deliveryTail.then(async () => {
12921
- if (viewArg.closing || this.browserViews.get(viewArg.id) !== viewArg) return;
12922
- await this.sendBrowserViewEvent(viewArg, queuedEvent);
12923
- }).catch((errorArg) => {
12924
- this.scheduleBrowserViewClose(viewArg, errorArg);
14316
+ const acknowledgement: IControllerBrowserAcknowledgementOperation = {
14317
+ operationId: queuedArg.operationId,
14318
+ terminal: false,
14319
+ operation: Promise.resolve(),
14320
+ };
14321
+ transportArg.activeAcknowledgements.set(queuedArg.operationId, acknowledgement);
14322
+ const operation = (async () => {
14323
+ try {
14324
+ await this.assertBrowserViewAuthority(
14325
+ viewArg,
14326
+ transportArg.generation,
14327
+ transportArg.identity,
14328
+ transportArg.abortController.signal,
14329
+ );
14330
+ const accepted = await viewArg.handle.lease.acknowledgeFrame(
14331
+ operationRequest.acknowledgement,
14332
+ );
14333
+ if (!acknowledgement.terminal) {
14334
+ acknowledgement.terminal = true;
14335
+ this.queueBrowserOperationResult(viewArg, transportArg, queuedArg.operationId, {
14336
+ status: 'succeeded',
14337
+ accepted,
14338
+ });
14339
+ }
14340
+ } catch (errorArg) {
14341
+ if (!acknowledgement.terminal) {
14342
+ acknowledgement.terminal = true;
14343
+ this.queueBrowserOperationResult(viewArg, transportArg, queuedArg.operationId, {
14344
+ status: 'failed',
14345
+ error: this.browserOperationError(errorArg),
14346
+ });
14347
+ }
14348
+ } finally {
14349
+ transportArg.activeAcknowledgements.delete(queuedArg.operationId);
14350
+ }
14351
+ })();
14352
+ acknowledgement.operation = operation;
14353
+ }
14354
+
14355
+ private settleBrowserOperation(
14356
+ viewArg: IControllerBrowserView,
14357
+ transportArg: IControllerBrowserViewTransport,
14358
+ queuedArg: IControllerBrowserQueuedOperation,
14359
+ resultArg: IControllerBrowserOperationResultPacket['result'],
14360
+ ): void {
14361
+ if (queuedArg.terminal) return;
14362
+ queuedArg.terminal = true;
14363
+ this.queueBrowserOperationResult(viewArg, transportArg, queuedArg.operationId, resultArg);
14364
+ }
14365
+
14366
+ private queueBrowserOperationResult(
14367
+ viewArg: IControllerBrowserView,
14368
+ transportArg: IControllerBrowserViewTransport,
14369
+ operationIdArg: number,
14370
+ resultArg: IControllerBrowserOperationResultPacket['result'],
14371
+ ): void {
14372
+ transportArg.cancellableOperationIds.delete(operationIdArg);
14373
+ try {
14374
+ this.queueBrowserServerPacket(viewArg, transportArg, {
14375
+ protocol: controllerBrowserTransportProtocol,
14376
+ streamGeneration: transportArg.generation,
14377
+ type: 'operationResult',
14378
+ operationId: operationIdArg,
14379
+ result: resultArg,
14380
+ });
14381
+ } catch (errorArg) {
14382
+ this.noteBrowserTransportFault(viewArg, transportArg, errorArg, false);
14383
+ }
14384
+ }
14385
+
14386
+ private queueBrowserServerPacket(
14387
+ viewArg: IControllerBrowserView,
14388
+ transportArg: IControllerBrowserViewTransport,
14389
+ packetArg: TControllerBrowserServerPacket,
14390
+ rawFrameArg?: Uint8Array,
14391
+ ): Promise<void> | undefined {
14392
+ if (
14393
+ viewArg.closing
14394
+ || viewArg.transport !== transportArg
14395
+ || !this.browserTransportCanDeliverPacket(transportArg, packetArg)
14396
+ ) return undefined;
14397
+ const packetBytes = encodeControllerBrowserTransportPacket(packetArg);
14398
+ const eventBytes = packetBytes.byteLength + (rawFrameArg?.byteLength ?? 0);
14399
+ if (
14400
+ transportArg.pendingEventCount >= controllerBrowserTransportMaximumPendingEvents
14401
+ || transportArg.pendingEventBytes + eventBytes
14402
+ > controllerBrowserTransportMaximumPendingEventBytes
14403
+ ) throw new Error('The browser view delivery queue is full.');
14404
+ transportArg.pendingEventCount += 1;
14405
+ transportArg.pendingEventBytes += eventBytes;
14406
+ const delivery = transportArg.deliveryTail.then(async () => {
14407
+ if (
14408
+ viewArg.closing
14409
+ || viewArg.transport !== transportArg
14410
+ || !this.browserTransportCanDeliverPacket(transportArg, packetArg)
14411
+ ) return;
14412
+ await transportArg.events.send(packetBytes);
14413
+ if (rawFrameArg) await transportArg.events.send(rawFrameArg);
14414
+ });
14415
+ transportArg.deliveryTail = delivery.then(() => undefined, () => undefined);
14416
+ void delivery.catch((errorArg) => {
14417
+ this.noteBrowserTransportFault(viewArg, transportArg, errorArg, true);
12925
14418
  }).finally(() => {
12926
- viewArg.pendingEventCount -= 1;
12927
- viewArg.pendingEventBytes -= eventBytes;
14419
+ transportArg.pendingEventCount -= 1;
14420
+ transportArg.pendingEventBytes -= eventBytes;
12928
14421
  });
12929
- viewArg.deliveryTail = delivery;
14422
+ return delivery;
14423
+ }
14424
+
14425
+ private browserTransportCanDeliverPacket(
14426
+ transportArg: IControllerBrowserViewTransport,
14427
+ packetArg: TControllerBrowserServerPacket,
14428
+ ): boolean {
14429
+ return transportArg.lifecycle === 'active'
14430
+ || (transportArg.lifecycle === 'ready' && packetArg.type === 'operationResult');
12930
14431
  }
12931
14432
 
12932
- private async sendBrowserViewEvent(
14433
+ private failBrowserTransportProtocol(
12933
14434
  viewArg: IControllerBrowserView,
12934
- eventArg: plugins.browserRuntime.TBrowserRuntimeEvent,
14435
+ transportArg: IControllerBrowserViewTransport,
14436
+ reasonArg: unknown,
14437
+ ): void {
14438
+ if (transportArg.lifecycle === 'retiring' || transportArg.lifecycle === 'retired') return;
14439
+ transportArg.lifecycle = 'failed';
14440
+ transportArg.failure = reasonArg;
14441
+ this.terminalizeBrowserTransportOperations(transportArg, reasonArg);
14442
+ void transportArg.events.abort(reasonArg).catch(() => undefined);
14443
+ void transportArg.operations.abort(reasonArg).catch(() => undefined);
14444
+ if (viewArg.transport !== transportArg) return;
14445
+ this.scheduleBrowserViewClose(viewArg, reasonArg);
14446
+ }
14447
+
14448
+ private noteBrowserTransportFault(
14449
+ viewArg: IControllerBrowserView,
14450
+ transportArg: IControllerBrowserViewTransport,
14451
+ reasonArg: unknown,
14452
+ recoverableArg: boolean,
14453
+ ): void {
14454
+ if (
14455
+ transportArg.lifecycle === 'failed'
14456
+ || transportArg.lifecycle === 'retiring'
14457
+ || transportArg.lifecycle === 'retired'
14458
+ ) return;
14459
+ transportArg.lifecycle = 'failed';
14460
+ transportArg.failure = reasonArg;
14461
+ this.terminalizeBrowserTransportOperations(transportArg, reasonArg);
14462
+ void transportArg.events.abort(reasonArg).catch(() => undefined);
14463
+ void transportArg.operations.abort(reasonArg).catch(() => undefined);
14464
+ if (viewArg.transport !== transportArg) return;
14465
+ if (
14466
+ recoverableArg
14467
+ && !viewArg.closing
14468
+ && viewArg.transport === transportArg
14469
+ && viewArg.lifecycle === 'active'
14470
+ ) {
14471
+ viewArg.lifecycle = 'recoverable';
14472
+ viewArg.recoveryDeadlineAt = Date.now() + controllerBrowserTransportRecoveryGraceMs;
14473
+ this.armBrowserViewRecoveryDeadline(viewArg);
14474
+ return;
14475
+ }
14476
+ this.scheduleBrowserViewClose(viewArg, reasonArg);
14477
+ }
14478
+
14479
+ private terminalizeBrowserTransportOperations(
14480
+ transportArg: IControllerBrowserViewTransport,
14481
+ reasonArg: unknown,
14482
+ ): void {
14483
+ if (!transportArg.abortController.signal.aborted) {
14484
+ transportArg.abortController.abort(reasonArg);
14485
+ }
14486
+ for (const queued of transportArg.operationQueue.splice(0)) queued.terminal = true;
14487
+ transportArg.queuedOperationBytes = 0;
14488
+ transportArg.cancellableOperationIds.clear();
14489
+ if (transportArg.activeMutation) {
14490
+ transportArg.activeMutation.cancelled = true;
14491
+ transportArg.activeMutation.abortController?.abort(reasonArg);
14492
+ }
14493
+ for (const acknowledgement of transportArg.activeAcknowledgements.values()) {
14494
+ acknowledgement.terminal = true;
14495
+ }
14496
+ }
14497
+
14498
+ private armBrowserViewRecoveryDeadline(viewArg: IControllerBrowserView): void {
14499
+ if (viewArg.recoveryTimer || viewArg.recoveryDeadlineAt === undefined) return;
14500
+ const delayMs = Math.max(0, viewArg.recoveryDeadlineAt - Date.now());
14501
+ const timer = setTimeout(() => {
14502
+ if (viewArg.recoveryTimer === timer) viewArg.recoveryTimer = undefined;
14503
+ if (viewArg.closing || viewArg.lifecycle === 'active') return;
14504
+ this.scheduleBrowserViewClose(viewArg, new Error('Browser view recovery timed out.'));
14505
+ }, delayMs);
14506
+ viewArg.recoveryTimer = timer;
14507
+ timer.unref?.();
14508
+ }
14509
+
14510
+ private retireBrowserViewTransport(
14511
+ viewArg: IControllerBrowserView,
14512
+ transportArg: IControllerBrowserViewTransport,
14513
+ deadlineAtArg: number,
12935
14514
  ): Promise<void> {
12936
- const typedSocket = this.typedServer?.typedsocket;
12937
- if (!typedSocket) throw new Error('The controller transport is unavailable.');
12938
- if (eventArg.type !== 'frame') {
12939
- const event: IReq_ControllerBrowserViewEvent['request']['event'] = eventArg.type === 'error'
12940
- ? {
12941
- type: 'error',
12942
- error: { ...eventArg.error, message: eventArg.error.code },
14515
+ if (transportArg.lifecycle === 'retired') return Promise.resolve();
14516
+ if (transportArg.retirementTask) return transportArg.retirementTask;
14517
+ if (transportArg.retirementSettlement && !transportArg.retirementSettled) {
14518
+ return Promise.reject(new Error('Browser stream generation cleanup remains pending.'));
14519
+ }
14520
+ if (!transportArg.retirementSettlement) {
14521
+ transportArg.lifecycle = 'retiring';
14522
+ const retirementReason = new Error('The browser stream generation was retired.');
14523
+ this.terminalizeBrowserTransportOperations(transportArg, retirementReason);
14524
+ const settlement = Promise.allSettled([
14525
+ Promise.resolve().then(() => transportArg.events.abort(retirementReason)),
14526
+ Promise.resolve().then(() => transportArg.operations.abort(retirementReason)),
14527
+ transportArg.openingTask,
14528
+ transportArg.receiveTask,
14529
+ transportArg.drainTask,
14530
+ transportArg.deliveryTail,
14531
+ ...[...transportArg.activeAcknowledgements.values()].map((entryArg) => entryArg.operation),
14532
+ ].filter((candidateArg): candidateArg is Promise<void> => candidateArg !== undefined))
14533
+ .then(() => {
14534
+ transportArg.retirementSettled = true;
14535
+ transportArg.lifecycle = 'retired';
14536
+ const stillAssociated = viewArg.transport === transportArg;
14537
+ const slot = this.browserViewSlots.get(viewArg.peerId);
14538
+ if (stillAssociated && viewArg.closing) viewArg.transport = undefined;
14539
+ if (
14540
+ transportArg.retirementDeadlineExpired
14541
+ && stillAssociated
14542
+ && viewArg.closing
14543
+ && slot?.view === viewArg
14544
+ ) {
14545
+ void this.requestBrowserViewCleanup(slot, viewArg, retirementReason).catch((errorArg) => {
14546
+ console.error(`Browser view ${viewArg.id} late cleanup failed.`, errorArg);
14547
+ });
12943
14548
  }
12944
- : {
12945
- type: 'state',
12946
- state: this.toControllerBrowserViewState(eventArg.state),
12947
- };
12948
- await typedSocket.createTypedRequest<IReq_ControllerBrowserViewEvent>(
12949
- 'controller.browser.view.event',
12950
- viewArg.transportConnection,
12951
- { timeoutMs: 5_000 },
12952
- ).fire({
12953
- viewId: viewArg.id,
12954
- resourceId: viewArg.resource.id,
12955
- attachmentRevision: viewArg.resource.attachmentRevision,
12956
- event,
12957
- }, { timeoutMs: 5_000, maxRetries: 0 });
12958
- return;
14549
+ });
14550
+ transportArg.retirementSettlement = settlement;
12959
14551
  }
12960
- const { data, ...frame } = eventArg.frame;
12961
- const integrity: plugins.typedrequestInterfaces.IVirtualStreamIntegrity = Object.freeze({
12962
- algorithm: 'sha256',
12963
- byteLength: data.byteLength,
12964
- digest: `sha256:${plugins.crypto.createHash('sha256').update(data).digest('hex')}`,
12965
- });
12966
- const authorization: IControllerBrowserFrameStreamAuthorization = Object.freeze({
12967
- marker: browserFrameStreamAuthorizationMarker,
12968
- view: viewArg,
12969
- authority: viewArg.authority,
12970
- transportPeer: viewArg.transportPeer,
12971
- contentType: frame.mimeType,
12972
- integrity,
14552
+ if (transportArg.retirementSettled) return Promise.resolve();
14553
+ const retirement = this.waitForBrowserTransportSettlement(
14554
+ transportArg.retirementSettlement,
14555
+ deadlineAtArg,
14556
+ ).catch((errorArg) => {
14557
+ transportArg.retirementDeadlineExpired = true;
14558
+ throw errorArg;
12973
14559
  });
12974
- const stream = typedSocket.createVirtualStream({
12975
- target: viewArg.transportConnection,
12976
- creatorDirection: 'send',
12977
- contentType: frame.mimeType,
12978
- integrity,
12979
- authorization,
14560
+ let tracked!: Promise<void>;
14561
+ tracked = retirement.finally(() => {
14562
+ if (transportArg.retirementTask === tracked) transportArg.retirementTask = undefined;
12980
14563
  });
12981
- viewArg.activeFrameStreams.add(stream);
12982
- const event: IReq_ControllerBrowserViewEvent['request']['event'] = {
12983
- type: 'frame',
12984
- frame: { ...frame, dataStream: stream },
12985
- };
12986
- const push = typedSocket.createTypedRequest<IReq_ControllerBrowserViewEvent>(
12987
- 'controller.browser.view.event',
12988
- viewArg.transportConnection,
12989
- { timeoutMs: 5_000 },
12990
- ).fire({
12991
- viewId: viewArg.id,
12992
- resourceId: viewArg.resource.id,
12993
- attachmentRevision: viewArg.resource.attachmentRevision,
12994
- event,
12995
- }, { timeoutMs: 5_000, maxRetries: 0 });
12996
- const production = (async () => {
12997
- await stream.send(data);
12998
- await stream.close();
12999
- })();
14564
+ transportArg.retirementTask = tracked;
14565
+ return tracked;
14566
+ }
14567
+
14568
+ private async waitForBrowserTransportSettlement(
14569
+ settlementArg: Promise<void>,
14570
+ deadlineAtArg: number,
14571
+ ): Promise<void> {
14572
+ const remainingMs = deadlineAtArg - Date.now();
14573
+ if (remainingMs <= 0) throw new Error('Browser stream generation cleanup timed out.');
14574
+ let timeout: ReturnType<typeof setTimeout> | undefined;
13000
14575
  try {
13001
- await Promise.all([push, production]);
13002
- } catch (errorArg) {
13003
- await stream.abort(errorArg).catch(() => undefined);
13004
- await Promise.allSettled([push, production]);
13005
- throw errorArg;
14576
+ await Promise.race([
14577
+ settlementArg,
14578
+ new Promise<never>((_resolve, reject) => {
14579
+ timeout = setTimeout(
14580
+ () => reject(new Error('Browser stream generation cleanup timed out.')),
14581
+ remainingMs,
14582
+ );
14583
+ timeout.unref?.();
14584
+ }),
14585
+ ]);
13006
14586
  } finally {
13007
- viewArg.activeFrameStreams.delete(stream);
14587
+ if (timeout) clearTimeout(timeout);
13008
14588
  }
13009
14589
  }
13010
14590
 
13011
- private bindBrowserFrameStreamAuthorization(
14591
+ private browserOperationError(errorArg: unknown): { code: string; message: string } {
14592
+ const typedError = asTypedResponseError(errorArg);
14593
+ const code = typeof typedError.errorData?.code === 'string'
14594
+ ? typedError.errorData.code.slice(0, 128)
14595
+ : 'browser_operation_failed';
14596
+ return { code, message: typedError.message.slice(0, 512) };
14597
+ }
14598
+
14599
+ private bindBrowserStreamAuthorization(
13012
14600
  authorizationArg: unknown,
13013
14601
  contextArg: Readonly<plugins.typedsocket.IVirtualStreamAuthorizationContext>,
13014
14602
  ): plugins.typedsocket.IVirtualStreamAuthorizationBinding {
13015
14603
  if (typeof authorizationArg !== 'object' || authorizationArg === null) {
13016
- throw new Error('Unknown browser frame stream authority.');
14604
+ throw new Error('Unknown browser stream authority.');
13017
14605
  }
13018
- const authorization = authorizationArg as Partial<IControllerBrowserFrameStreamAuthorization>;
14606
+ const authorization = authorizationArg as Partial<IControllerBrowserStreamAuthorization>;
13019
14607
  const view = authorization.view;
13020
14608
  if (
13021
- authorization.marker !== browserFrameStreamAuthorizationMarker
14609
+ authorization.marker !== browserStreamAuthorizationMarker
13022
14610
  || !view
13023
14611
  || authorization.authority !== view.authority
13024
14612
  || authorization.transportPeer !== view.transportPeer
14613
+ || typeof authorization.transportIdentity !== 'symbol'
13025
14614
  || contextArg.target !== view.transportPeer
13026
- || contextArg.creatorDirection !== 'send'
14615
+ || authorization.streamGeneration !== view.streamGeneration
14616
+ || contextArg.creatorDirection !== authorization.creatorDirection
13027
14617
  || contextArg.contentType !== authorization.contentType
13028
- || contextArg.integrity?.algorithm !== authorization.integrity?.algorithm
13029
- || contextArg.integrity?.byteLength !== authorization.integrity?.byteLength
13030
- || contextArg.integrity?.digest !== authorization.integrity?.digest
14618
+ || contextArg.integrity !== undefined
13031
14619
  || view.closing
13032
- || view.activationState === 'inactive'
13033
14620
  || this.browserViews.get(view.id) !== view
13034
14621
  || this.browserViewSlots.get(view.peerId)?.view !== view
13035
- || !this.isBrowserRuntimeAuthorityCurrent(view)
13036
- ) throw new Error('The browser frame stream authority is unavailable.');
13037
- const bound = authorization as IControllerBrowserFrameStreamAuthorization;
14622
+ || !this.isBrowserStreamBindingAuthorityCurrent(
14623
+ view,
14624
+ authorization.streamGeneration,
14625
+ authorization.transportIdentity,
14626
+ )
14627
+ ) throw new Error('The browser stream authority is unavailable.');
14628
+ const bound = authorization as IControllerBrowserStreamAuthorization;
13038
14629
  return {
13039
- revalidate: (revalidationArg) => this.revalidateBrowserFrameStream(
14630
+ revalidate: (revalidationArg) => this.revalidateBrowserStream(
13040
14631
  bound,
13041
14632
  revalidationArg,
13042
14633
  ),
13043
14634
  };
13044
14635
  }
13045
14636
 
13046
- private async revalidateBrowserFrameStream(
13047
- authorizationArg: IControllerBrowserFrameStreamAuthorization,
14637
+ private async revalidateBrowserStream(
14638
+ authorizationArg: IControllerBrowserStreamAuthorization,
13048
14639
  contextArg: Readonly<plugins.typedsocket.IVirtualStreamRevalidationContext>,
13049
14640
  ): Promise<boolean> {
13050
14641
  const view = authorizationArg.view;
@@ -13054,32 +14645,93 @@ export class OpenCodeController {
13054
14645
  || contextArg.connection.peer !== authorizationArg.transportPeer
13055
14646
  || view.transportPeer !== authorizationArg.transportPeer
13056
14647
  || view.authority !== authorizationArg.authority
13057
- || view.closing
13058
- || view.activationState === 'inactive'
13059
- || this.browserViews.get(view.id) !== view
13060
- || this.browserViewSlots.get(view.peerId)?.view !== view
13061
- || !this.authenticatedPeerIds.has(view.peerId)
13062
- || typeof view.transportPeer.context.state[authenticatedPeerStateKey] !== 'string'
14648
+ || view.streamGeneration !== authorizationArg.streamGeneration
14649
+ || !this.isBrowserViewInMemoryAuthorityCurrent(
14650
+ view,
14651
+ authorizationArg.streamGeneration,
14652
+ authorizationArg.transportIdentity,
14653
+ )
13063
14654
  || view.transportPeer.context.headers.get('origin') !== this.requireRuntimeConfig().publicOrigin
13064
- || !this.isBrowserRuntimeAuthorityCurrent(view)
13065
14655
  ) return false;
13066
14656
  try {
13067
- const current = await awaitWithAbortSignal(this.requireAuthStore().getResource(
13068
- view.resource.projectId,
13069
- view.resource.id,
13070
- ), contextArg.abortSignal);
13071
- return current !== undefined
13072
- && current.kind === 'browser'
13073
- && current.lifecycle === 'active'
13074
- && current.pendingAttachment === undefined
13075
- && current.attachmentAuthorityId === view.resource.attachmentAuthorityId
13076
- && current.attachmentRevision === view.resource.attachmentRevision
13077
- && this.isBrowserRuntimeAuthorityCurrent(view);
14657
+ await this.assertBrowserViewAuthority(
14658
+ view,
14659
+ authorizationArg.streamGeneration,
14660
+ authorizationArg.transportIdentity,
14661
+ contextArg.abortSignal,
14662
+ );
14663
+ return true;
13078
14664
  } catch {
13079
14665
  return false;
13080
14666
  }
13081
14667
  }
13082
14668
 
14669
+ private isBrowserStreamBindingAuthorityCurrent(
14670
+ viewArg: IControllerBrowserView,
14671
+ streamGenerationArg: number,
14672
+ transportIdentityArg: symbol,
14673
+ ): boolean {
14674
+ return !viewArg.closing
14675
+ && viewArg.lifecycle !== 'closing'
14676
+ && viewArg.streamGeneration === streamGenerationArg
14677
+ && viewArg.pendingTransportIdentity === transportIdentityArg
14678
+ && this.browserViews.get(viewArg.id) === viewArg
14679
+ && this.browserViewSlots.get(viewArg.peerId)?.view === viewArg
14680
+ && this.authenticatedPeerIds.has(viewArg.peerId)
14681
+ && viewArg.transportPeer.context.state[authenticatedPeerStateKey] === viewArg.credentialId
14682
+ && this.isBrowserRuntimeAuthorityCurrent(viewArg);
14683
+ }
14684
+
14685
+ private isBrowserViewInMemoryAuthorityCurrent(
14686
+ viewArg: IControllerBrowserView,
14687
+ streamGenerationArg: number,
14688
+ transportIdentityArg: symbol,
14689
+ ): boolean {
14690
+ return !viewArg.closing
14691
+ && viewArg.lifecycle !== 'closing'
14692
+ && viewArg.streamGeneration === streamGenerationArg
14693
+ && viewArg.transport?.identity === transportIdentityArg
14694
+ && this.browserViews.get(viewArg.id) === viewArg
14695
+ && this.browserViewSlots.get(viewArg.peerId)?.view === viewArg
14696
+ && this.authenticatedPeerIds.has(viewArg.peerId)
14697
+ && viewArg.transportPeer.context.state[authenticatedPeerStateKey] === viewArg.credentialId
14698
+ && this.isBrowserRuntimeAuthorityCurrent(viewArg);
14699
+ }
14700
+
14701
+ private async assertBrowserViewAuthority(
14702
+ viewArg: IControllerBrowserView,
14703
+ streamGenerationArg: number,
14704
+ transportIdentityArg: symbol,
14705
+ signalArg: AbortSignal,
14706
+ ): Promise<void> {
14707
+ signalArg.throwIfAborted();
14708
+ if (!this.isBrowserViewInMemoryAuthorityCurrent(
14709
+ viewArg,
14710
+ streamGenerationArg,
14711
+ transportIdentityArg,
14712
+ )) {
14713
+ throw new ControllerResourceUnavailableError('The browser view authority is unavailable.');
14714
+ }
14715
+ const current = await awaitWithAbortSignal(this.requireAuthStore().getResource(
14716
+ viewArg.resource.projectId,
14717
+ viewArg.resource.id,
14718
+ ), signalArg);
14719
+ signalArg.throwIfAborted();
14720
+ if (
14721
+ !current
14722
+ || current.kind !== 'browser'
14723
+ || current.lifecycle !== 'active'
14724
+ || current.pendingAttachment
14725
+ || current.attachmentAuthorityId !== viewArg.resource.attachmentAuthorityId
14726
+ || current.attachmentRevision !== viewArg.resource.attachmentRevision
14727
+ || !this.isBrowserViewInMemoryAuthorityCurrent(
14728
+ viewArg,
14729
+ streamGenerationArg,
14730
+ transportIdentityArg,
14731
+ )
14732
+ ) throw new ControllerResourceUnavailableError('The browser resource changed.');
14733
+ }
14734
+
13083
14735
  private isBrowserRuntimeAuthorityCurrent(viewArg: IControllerBrowserView): boolean {
13084
14736
  try {
13085
14737
  return viewArg.handle.lease.isAuthorityCurrent(viewArg.authority);
@@ -13088,11 +14740,14 @@ export class OpenCodeController {
13088
14740
  }
13089
14741
  }
13090
14742
 
13091
- private toControllerBrowserViewState(
14743
+ private nextControllerBrowserViewState(
14744
+ viewArg: IControllerBrowserView,
13092
14745
  stateArg: plugins.browserRuntime.IBrowserRuntimeState,
13093
- ): IReq_ControllerBrowserViewOpen['response']['state'] {
14746
+ ): IControllerBrowserViewState {
13094
14747
  const { lastError, ...state } = stateArg;
14748
+ viewArg.stateRevision += 1;
13095
14749
  return {
14750
+ revision: viewArg.stateRevision,
13096
14751
  ...state,
13097
14752
  ...(lastError === undefined ? {} : {
13098
14753
  lastError: { ...lastError, message: lastError.code },