agl 22.0.3 → 23.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.
@@ -12,6 +12,7 @@ import {
12
12
  controllerBuiltinCommands,
13
13
  controllerMaxToolEventBytes,
14
14
  controllerMaxLiveMessageEventBytes,
15
+ controllerLiveMessageDeltaTargetBytes,
15
16
  controllerInitialMessageBundleLimit,
16
17
  controllerMaximumTranscriptBytes,
17
18
  controllerLiveToolPayloadNotice,
@@ -26,8 +27,11 @@ import {
26
27
  type IControllerMessagePage,
27
28
  type IControllerMessageStreamCursor,
28
29
  type IControllerPendingPrompt,
30
+ type IControllerReasoningDelta,
29
31
  type IControllerReasoningUpdate,
32
+ type IControllerTextDelta,
30
33
  type IControllerTextUpdate,
34
+ type IControllerTodo,
31
35
  type TControllerModelChoice,
32
36
  type IControllerModelRefreshJob,
33
37
  type IControllerPermission,
@@ -539,15 +543,24 @@ interface ILatestToolExecution {
539
543
  interface ILatestReasoningUpdate {
540
544
  projectId: string;
541
545
  update: IControllerReasoningUpdate;
546
+ textUtf8Bytes: number;
542
547
  bytes?: number;
543
548
  }
544
549
 
545
550
  interface ILatestTextUpdate {
546
551
  projectId: string;
547
552
  update: IControllerTextUpdate;
553
+ textUtf8Bytes: number;
548
554
  bytes?: number;
549
555
  }
550
556
 
557
+ type TControllerMessageDelta = IControllerReasoningDelta | IControllerTextDelta;
558
+
559
+ interface IControllerTextDeltaFragment {
560
+ delta: string;
561
+ utf8Bytes: number;
562
+ }
563
+
551
564
  type TControllerToolExecutionSnapshot = Omit<
552
565
  IControllerToolExecution,
553
566
  'revision' | 'streamEpoch'
@@ -557,6 +570,103 @@ const historyLimitedNotice = '[earlier message content omitted: this transcript
557
570
  const historyLimitedDigestPrefix = 'history-limited:';
558
571
  const liveToolExecutionBytes = controllerMaxToolEventBytes - (8 * 1024);
559
572
 
573
+ const controllerEventMessageDelta = (
574
+ eventArg: IControllerEvent,
575
+ ): TControllerMessageDelta | undefined => eventArg.reasoningDelta ?? eventArg.textDelta;
576
+
577
+ const controllerTextAfterAppendUtf8Bytes = (
578
+ textArg: string,
579
+ textUtf8BytesArg: number,
580
+ deltaArg: string,
581
+ ): number | undefined => {
582
+ // Separately encoded surrogate halves cost six bytes; after append the pair costs four.
583
+ const boundaryCorrection = (
584
+ textArg.length > 0
585
+ && deltaArg.length > 0
586
+ && textArg.charCodeAt(textArg.length - 1) >= 0xD800
587
+ && textArg.charCodeAt(textArg.length - 1) <= 0xDBFF
588
+ && deltaArg.charCodeAt(0) >= 0xDC00
589
+ && deltaArg.charCodeAt(0) <= 0xDFFF
590
+ ) ? -2 : 0;
591
+ const result = textUtf8BytesArg + Buffer.byteLength(deltaArg, 'utf8') + boundaryCorrection;
592
+ return Number.isSafeInteger(result) && result >= 0 ? result : undefined;
593
+ };
594
+
595
+ const fragmentControllerTextDelta = (valueArg: string): IControllerTextDeltaFragment[] => {
596
+ const fragments: IControllerTextDeltaFragment[] = [];
597
+ let delta = '';
598
+ let utf8Bytes = 0;
599
+ for (const codePoint of valueArg) {
600
+ const codePointBytes = Buffer.byteLength(codePoint, 'utf8');
601
+ if (
602
+ delta !== ''
603
+ && utf8Bytes + codePointBytes > controllerLiveMessageDeltaTargetBytes
604
+ ) {
605
+ fragments.push({ delta, utf8Bytes });
606
+ delta = '';
607
+ utf8Bytes = 0;
608
+ }
609
+ delta += codePoint;
610
+ utf8Bytes += codePointBytes;
611
+ }
612
+ if (delta !== '') fragments.push({ delta, utf8Bytes });
613
+ return fragments;
614
+ };
615
+
616
+ const controllerMessageDeltaPendingKey = (eventArg: IControllerEvent): string | undefined => {
617
+ const delta = controllerEventMessageDelta(eventArg);
618
+ if (!delta) return undefined;
619
+ return JSON.stringify([
620
+ eventArg.type,
621
+ eventArg.harnessId ?? null,
622
+ eventArg.projectId ?? null,
623
+ controllerRuntimeIdKey(delta.sessionId),
624
+ controllerRuntimeIdKey(delta.partId),
625
+ ]);
626
+ };
627
+
628
+ const controllerMessageDeltaEventsAreContiguous = (
629
+ previousEventArg: IControllerEvent,
630
+ eventArg: IControllerEvent,
631
+ ): boolean => {
632
+ const previous = controllerEventMessageDelta(previousEventArg);
633
+ const delta = controllerEventMessageDelta(eventArg);
634
+ if (!previous || !delta) return false;
635
+ return previousEventArg.type === eventArg.type
636
+ && previousEventArg.harnessId === eventArg.harnessId
637
+ && previousEventArg.projectId === eventArg.projectId
638
+ && (previousEventArg.sessionId
639
+ ? eventArg.sessionId !== undefined
640
+ && controllerRuntimeIdKey(previousEventArg.sessionId)
641
+ === controllerRuntimeIdKey(eventArg.sessionId)
642
+ : eventArg.sessionId === undefined)
643
+ && controllerRuntimeIdKey(previous.sessionId) === controllerRuntimeIdKey(delta.sessionId)
644
+ && controllerRuntimeIdKey(previous.messageId) === controllerRuntimeIdKey(delta.messageId)
645
+ && controllerRuntimeIdKey(previous.partId) === controllerRuntimeIdKey(delta.partId)
646
+ && previous.streamEpoch === delta.streamEpoch
647
+ && previous.order?.messageIndex === delta.order?.messageIndex
648
+ && previous.order?.partIndex === delta.order?.partIndex
649
+ && previous.textUtf8Bytes === delta.baseTextUtf8Bytes
650
+ && delta.revision > previous.revision;
651
+ };
652
+
653
+ const mergeControllerMessageDeltaEvents = (
654
+ previousEventArg: IControllerEvent,
655
+ eventArg: IControllerEvent,
656
+ ): IControllerEvent | undefined => {
657
+ if (!controllerMessageDeltaEventsAreContiguous(previousEventArg, eventArg)) return undefined;
658
+ const previous = controllerEventMessageDelta(previousEventArg)!;
659
+ const delta = controllerEventMessageDelta(eventArg)!;
660
+ const mergedDelta = {
661
+ ...delta,
662
+ delta: `${previous.delta}${delta.delta}`,
663
+ baseTextUtf8Bytes: previous.baseTextUtf8Bytes,
664
+ };
665
+ return eventArg.reasoningDelta
666
+ ? { ...eventArg, reasoningDelta: mergedDelta }
667
+ : { ...eventArg, textDelta: mergedDelta };
668
+ };
669
+
560
670
  const markHistoryLimitedDigest = (digestArg: string): string => (
561
671
  digestArg.startsWith(historyLimitedDigestPrefix)
562
672
  ? digestArg
@@ -8978,14 +9088,36 @@ export class OpenCodeController {
8978
9088
  messagePage: IControllerMessagePage;
8979
9089
  permissions: IControllerPermission[];
8980
9090
  questions: IControllerQuestion[];
8981
- todos: [];
9091
+ todos: IControllerTodo[];
8982
9092
  model?: string;
8983
9093
  effort?: string;
8984
9094
  }> {
8985
9095
  const reader = this.requireFlexProjectionReader();
8986
9096
  const supervisor = this.flexSupervisor;
8987
- const [session, messagePage, permissionResult] = await Promise.all([
8988
- reader.getSession(projectIdArg, sessionIdArg, signalArg),
9097
+ const sessionPromise = reader.getSession(projectIdArg, sessionIdArg, signalArg);
9098
+ const todosPromise: Promise<IControllerTodo[]> = sessionPromise.then(async (session) => {
9099
+ if (
9100
+ session.sessionGenerationId === undefined
9101
+ || session.sessionGenerationSequence === undefined
9102
+ ) return [];
9103
+ signalArg?.throwIfAborted();
9104
+ const tasks = await this.requireAuthStore().readFlexProjectTasks(
9105
+ projectIdArg,
9106
+ sessionIdArg,
9107
+ {
9108
+ sessionGenerationId: session.sessionGenerationId,
9109
+ sessionGenerationSequence: session.sessionGenerationSequence,
9110
+ },
9111
+ );
9112
+ signalArg?.throwIfAborted();
9113
+ return tasks.map((task) => ({
9114
+ id: flexRuntimeId(task.id),
9115
+ content: task.content,
9116
+ status: task.status,
9117
+ }));
9118
+ });
9119
+ const [session, messagePage, permissionResult, todos] = await Promise.all([
9120
+ sessionPromise,
8989
9121
  reader.listMessagePage(projectIdArg, sessionIdArg, {
8990
9122
  limit: controllerInitialMessageBundleLimit,
8991
9123
  }, signalArg),
@@ -8995,6 +9127,7 @@ export class OpenCodeController {
8995
9127
  familyRootSessionId: sessionIdArg,
8996
9128
  }, signalArg).catch(() => ({ permissions: [] as TFlexPermissionRequest[] }))
8997
9129
  : Promise.resolve({ permissions: [] as TFlexPermissionRequest[] }),
9130
+ todosPromise,
8998
9131
  ]);
8999
9132
  const bundles = messagePage.messages.map((message) => this.toFlexMessageBundle(message));
9000
9133
  const messages = bundles.flatMap((bundle) => bundle.messages);
@@ -9016,7 +9149,7 @@ export class OpenCodeController {
9016
9149
  }),
9017
9150
  permissions: permissionResult.permissions.map((permission) => this.toFlexPermission(permission)),
9018
9151
  questions: [],
9019
- todos: [],
9152
+ todos,
9020
9153
  ...(model === undefined ? {} : { model }),
9021
9154
  ...(effort === undefined ? {} : { effort }),
9022
9155
  };
@@ -9968,6 +10101,10 @@ export class OpenCodeController {
9968
10101
  this.handleFlexTextUpdate(eventArg);
9969
10102
  return;
9970
10103
  }
10104
+ if (eventArg.type === 'part.delta') {
10105
+ this.handleFlexPartDelta(eventArg);
10106
+ return;
10107
+ }
9971
10108
  if (eventArg.type === 'permissions.changed') {
9972
10109
  if (this.autoAcceptPermissions || this.hasFlexYoloSessions()) {
9973
10110
  this.queueFlexAutoAcceptSweep(eventArg.scopeId, eventArg.sessionId);
@@ -10389,7 +10526,12 @@ export class OpenCodeController {
10389
10526
  const bytes = Buffer.byteLength(JSON.stringify(eventArg), 'utf8');
10390
10527
  return {
10391
10528
  tool: eventArg.toolExecution ? bytes : 0,
10392
- message: eventArg.reasoningUpdate || eventArg.textUpdate ? bytes : 0,
10529
+ message: eventArg.reasoningUpdate
10530
+ || eventArg.reasoningDelta
10531
+ || eventArg.textUpdate
10532
+ || eventArg.textDelta
10533
+ ? bytes
10534
+ : 0,
10393
10535
  };
10394
10536
  }
10395
10537
 
@@ -10549,7 +10691,12 @@ export class OpenCodeController {
10549
10691
  this.latestFlexTextUpdates.clear();
10550
10692
  this.latestFlexMessageUpdateBytes = 0;
10551
10693
  for (const [key, event] of this.pendingFlexControllerEvents) {
10552
- if (event.reasoningUpdate || event.textUpdate) this.deletePendingFlexControllerEvent(key);
10694
+ if (
10695
+ event.reasoningUpdate
10696
+ || event.reasoningDelta
10697
+ || event.textUpdate
10698
+ || event.textDelta
10699
+ ) this.deletePendingFlexControllerEvent(key);
10553
10700
  }
10554
10701
  const timestamp = Date.now();
10555
10702
  const barrier: IControllerEvent = {
@@ -10635,11 +10782,19 @@ export class OpenCodeController {
10635
10782
  for (const [key, event] of this.pendingFlexControllerEvents) {
10636
10783
  if (
10637
10784
  event.projectId === projectIdArg
10638
- && (event.toolExecution || event.reasoningUpdate || event.textUpdate)
10785
+ && (
10786
+ event.toolExecution
10787
+ || event.reasoningUpdate
10788
+ || event.reasoningDelta
10789
+ || event.textUpdate
10790
+ || event.textDelta
10791
+ )
10639
10792
  && controllerRuntimeIdKey(
10640
10793
  event.toolExecution?.sessionId
10641
10794
  ?? event.reasoningUpdate?.sessionId
10642
- ?? event.textUpdate!.sessionId,
10795
+ ?? event.reasoningDelta?.sessionId
10796
+ ?? event.textUpdate?.sessionId
10797
+ ?? event.textDelta!.sessionId,
10643
10798
  ) === sessionKey
10644
10799
  ) this.deletePendingFlexControllerEvent(key);
10645
10800
  }
@@ -10705,7 +10860,13 @@ export class OpenCodeController {
10705
10860
  for (const [key, event] of this.pendingFlexControllerEvents) {
10706
10861
  if (
10707
10862
  event.projectId === projectIdArg
10708
- && (event.toolExecution || event.reasoningUpdate || event.textUpdate)
10863
+ && (
10864
+ event.toolExecution
10865
+ || event.reasoningUpdate
10866
+ || event.reasoningDelta
10867
+ || event.textUpdate
10868
+ || event.textDelta
10869
+ )
10709
10870
  ) {
10710
10871
  this.deletePendingFlexControllerEvent(key);
10711
10872
  }
@@ -10862,7 +11023,11 @@ export class OpenCodeController {
10862
11023
  }
10863
11024
  this.setLatestFlexReasoningUpdate(
10864
11025
  key,
10865
- { projectId: eventArg.scopeId, update },
11026
+ {
11027
+ projectId: eventArg.scopeId,
11028
+ update,
11029
+ textUtf8Bytes: Buffer.byteLength(update.text, 'utf8'),
11030
+ },
10866
11031
  eventBytes,
10867
11032
  );
10868
11033
  this.queueFlexControllerEvent(controllerEvent);
@@ -10922,12 +11087,169 @@ export class OpenCodeController {
10922
11087
  }
10923
11088
  this.setLatestFlexTextUpdate(
10924
11089
  key,
10925
- { projectId: eventArg.scopeId, update },
11090
+ {
11091
+ projectId: eventArg.scopeId,
11092
+ update,
11093
+ textUtf8Bytes: Buffer.byteLength(update.text, 'utf8'),
11094
+ },
10926
11095
  eventBytes,
10927
11096
  );
10928
11097
  this.queueFlexControllerEvent(controllerEvent);
10929
11098
  }
10930
11099
 
11100
+ private handleFlexPartDelta(
11101
+ eventArg: Extract<TFlexChildEvent, { type: 'part.delta' }>,
11102
+ ): void {
11103
+ if (this.lifecycleState !== 'starting' && this.lifecycleState !== 'ready') return;
11104
+ const sourceUpdatedAt = Date.parse(eventArg.timestamp);
11105
+ if (!Number.isFinite(sourceUpdatedAt)) {
11106
+ this.advanceFlexMessageStreamEpoch(true);
11107
+ return;
11108
+ }
11109
+ const sessionId = flexRuntimeId(eventArg.sessionId);
11110
+ const messageId = flexRuntimeId(eventArg.messageId);
11111
+ const partId = flexRuntimeId(eventArg.partId);
11112
+ const order = { messageIndex: eventArg.messageIndex, partIndex: eventArg.partIndex };
11113
+ const key = this.openCodeToolExecutionKey(eventArg.scopeId, { sessionId, partId });
11114
+ const cache = eventArg.partType === 'reasoning'
11115
+ ? this.latestFlexReasoningUpdates
11116
+ : this.latestFlexTextUpdates;
11117
+ const entry = cache.get(key);
11118
+ const previous = entry?.update;
11119
+ if (
11120
+ !entry
11121
+ || !previous
11122
+ || this.flexMessageStreamEpoch === 0
11123
+ || entry.projectId !== eventArg.scopeId
11124
+ || previous.status !== 'running'
11125
+ || previous.streamEpoch !== this.flexMessageStreamEpoch
11126
+ || previous.revision > this.flexMessageStreamRevision
11127
+ || controllerRuntimeIdKey(previous.sessionId) !== controllerRuntimeIdKey(sessionId)
11128
+ || controllerRuntimeIdKey(previous.messageId) !== controllerRuntimeIdKey(messageId)
11129
+ || controllerRuntimeIdKey(previous.partId) !== controllerRuntimeIdKey(partId)
11130
+ || previous.order?.messageIndex !== order.messageIndex
11131
+ || previous.order.partIndex !== order.partIndex
11132
+ || sourceUpdatedAt < previous.sourceUpdatedAt
11133
+ || entry.textUtf8Bytes !== eventArg.baseTextUtf8Bytes
11134
+ ) {
11135
+ this.advanceFlexMessageStreamEpoch(true);
11136
+ return;
11137
+ }
11138
+ const textUtf8Bytes = controllerTextAfterAppendUtf8Bytes(
11139
+ previous.text,
11140
+ entry.textUtf8Bytes,
11141
+ eventArg.delta,
11142
+ );
11143
+ if (
11144
+ textUtf8Bytes === undefined
11145
+ || textUtf8Bytes <= entry.textUtf8Bytes
11146
+ || textUtf8Bytes !== eventArg.textUtf8Bytes
11147
+ ) {
11148
+ this.advanceFlexMessageStreamEpoch(true);
11149
+ return;
11150
+ }
11151
+
11152
+ const revision = this.flexMessageStreamRevision + 1;
11153
+ const update = {
11154
+ ...previous,
11155
+ text: `${previous.text}${eventArg.delta}`,
11156
+ sourceUpdatedAt,
11157
+ revision,
11158
+ streamEpoch: this.flexMessageStreamEpoch,
11159
+ } as IControllerReasoningUpdate | IControllerTextUpdate;
11160
+ const controllerEvent: IControllerEvent = eventArg.partType === 'reasoning'
11161
+ ? {
11162
+ type: 'session.reasoning.delta',
11163
+ harnessId: 'flex',
11164
+ projectId: eventArg.scopeId,
11165
+ sessionId,
11166
+ reasoningDelta: {
11167
+ sessionId,
11168
+ messageId,
11169
+ partId,
11170
+ delta: eventArg.delta,
11171
+ baseTextUtf8Bytes: eventArg.baseTextUtf8Bytes,
11172
+ textUtf8Bytes,
11173
+ order,
11174
+ sourceUpdatedAt,
11175
+ revision,
11176
+ streamEpoch: this.flexMessageStreamEpoch,
11177
+ },
11178
+ timestamp: sourceUpdatedAt,
11179
+ }
11180
+ : {
11181
+ type: 'session.text.delta',
11182
+ harnessId: 'flex',
11183
+ projectId: eventArg.scopeId,
11184
+ sessionId,
11185
+ textDelta: {
11186
+ sessionId,
11187
+ messageId,
11188
+ partId,
11189
+ delta: eventArg.delta,
11190
+ baseTextUtf8Bytes: eventArg.baseTextUtf8Bytes,
11191
+ textUtf8Bytes,
11192
+ order,
11193
+ sourceUpdatedAt,
11194
+ revision,
11195
+ streamEpoch: this.flexMessageStreamEpoch,
11196
+ },
11197
+ timestamp: sourceUpdatedAt,
11198
+ };
11199
+ const snapshotEvent: IControllerEvent = eventArg.partType === 'reasoning'
11200
+ ? {
11201
+ type: 'session.reasoning.updated',
11202
+ harnessId: 'flex',
11203
+ projectId: eventArg.scopeId,
11204
+ sessionId,
11205
+ reasoningUpdate: update as IControllerReasoningUpdate,
11206
+ timestamp: sourceUpdatedAt,
11207
+ }
11208
+ : {
11209
+ type: 'session.text.updated',
11210
+ harnessId: 'flex',
11211
+ projectId: eventArg.scopeId,
11212
+ sessionId,
11213
+ textUpdate: update as IControllerTextUpdate,
11214
+ timestamp: sourceUpdatedAt,
11215
+ };
11216
+ const eventBytes = Buffer.byteLength(JSON.stringify(controllerEvent), 'utf8');
11217
+ const snapshotBytes = Buffer.byteLength(JSON.stringify(snapshotEvent), 'utf8');
11218
+ if (
11219
+ eventBytes > controllerMaxLiveMessageEventBytes
11220
+ || snapshotBytes > controllerMaxLiveMessageEventBytes
11221
+ || this.latestFlexMessageUpdateBytes - (entry.bytes ?? 0) + snapshotBytes
11222
+ > maxLatestFlexMessageUpdateBytes
11223
+ ) {
11224
+ this.advanceFlexMessageStreamEpoch(true);
11225
+ return;
11226
+ }
11227
+
11228
+ this.flexMessageStreamRevision = revision;
11229
+ if (eventArg.partType === 'reasoning') {
11230
+ this.setLatestFlexReasoningUpdate(
11231
+ key,
11232
+ {
11233
+ projectId: eventArg.scopeId,
11234
+ update: update as IControllerReasoningUpdate,
11235
+ textUtf8Bytes,
11236
+ },
11237
+ snapshotBytes,
11238
+ );
11239
+ } else {
11240
+ this.setLatestFlexTextUpdate(
11241
+ key,
11242
+ {
11243
+ projectId: eventArg.scopeId,
11244
+ update: update as IControllerTextUpdate,
11245
+ textUtf8Bytes,
11246
+ },
11247
+ snapshotBytes,
11248
+ );
11249
+ }
11250
+ this.queueFlexControllerEvent(controllerEvent);
11251
+ }
11252
+
10931
11253
  private getOpenCodeToolStreamCursor(): IControllerToolStreamCursor {
10932
11254
  return {
10933
11255
  streamEpoch: this.openCodeToolStreamEpoch,
@@ -11051,12 +11373,25 @@ export class OpenCodeController {
11051
11373
 
11052
11374
  private advanceOpenCodeMessageStreamEpoch(includeSessionsChangedArg: boolean): void {
11053
11375
  if (this.lifecycleState !== 'starting' && this.lifecycleState !== 'ready') return;
11376
+ const pendingEntries = includeSessionsChangedArg ? 2 : 1;
11377
+ if (
11378
+ this.pendingOpenCodeControllerEvents.size
11379
+ > maxPendingOpenCodeControllerEvents - pendingEntries
11380
+ ) {
11381
+ this.advanceOpenCodeStreams(includeSessionsChangedArg);
11382
+ return;
11383
+ }
11054
11384
  this.openCodeMessageStreamEpoch += 1;
11055
11385
  this.openCodeMessageStreamRevision = 0;
11056
11386
  this.latestOpenCodeReasoningUpdates.clear();
11057
11387
  this.latestOpenCodeTextUpdates.clear();
11058
11388
  for (const [key, event] of this.pendingOpenCodeControllerEvents) {
11059
- if (event.reasoningUpdate || event.textUpdate) this.pendingOpenCodeControllerEvents.delete(key);
11389
+ if (
11390
+ event.reasoningUpdate
11391
+ || event.reasoningDelta
11392
+ || event.textUpdate
11393
+ || event.textDelta
11394
+ ) this.pendingOpenCodeControllerEvents.delete(key);
11060
11395
  }
11061
11396
  const timestamp = Date.now();
11062
11397
  const barrier: IControllerEvent = {
@@ -11143,6 +11478,143 @@ export class OpenCodeController {
11143
11478
  if (!cache.has(key) && cache.size >= maxLatestOpenCodeMessageUpdates) {
11144
11479
  this.advanceOpenCodeMessageStreamEpoch(true);
11145
11480
  }
11481
+ const previousEntry = cache.get(key);
11482
+ const previous = previousEntry?.update;
11483
+ if (
11484
+ previousEntry !== undefined
11485
+ && previous !== undefined
11486
+ && previousEntry.projectId === projectId
11487
+ && previous.streamEpoch === this.openCodeMessageStreamEpoch
11488
+ && previous.revision <= this.openCodeMessageStreamRevision
11489
+ && controllerRuntimeIdKey(previous.sessionId) === controllerRuntimeIdKey(updateArg.sessionId)
11490
+ && controllerRuntimeIdKey(previous.messageId) === controllerRuntimeIdKey(updateArg.messageId)
11491
+ && controllerRuntimeIdKey(previous.partId) === controllerRuntimeIdKey(updateArg.partId)
11492
+ && previous.status === 'running'
11493
+ && updateArg.status === 'running'
11494
+ && updateArg.text.length > previous.text.length
11495
+ && updateArg.text.startsWith(previous.text)
11496
+ && Number.isSafeInteger(previousEntry.textUtf8Bytes)
11497
+ ) {
11498
+ const deltaText = updateArg.text.slice(previous.text.length);
11499
+ const fragments = fragmentControllerTextDelta(deltaText);
11500
+ const textUtf8Bytes = controllerTextAfterAppendUtf8Bytes(
11501
+ previous.text,
11502
+ previousEntry.textUtf8Bytes,
11503
+ deltaText,
11504
+ );
11505
+ const finalRevision = this.openCodeMessageStreamRevision + fragments.length;
11506
+ if (
11507
+ fragments.length === 0
11508
+ || textUtf8Bytes === undefined
11509
+ || textUtf8Bytes <= previousEntry.textUtf8Bytes
11510
+ || !Number.isSafeInteger(finalRevision)
11511
+ ) {
11512
+ this.advanceOpenCodeMessageStreamEpoch(true);
11513
+ return;
11514
+ }
11515
+ const update = {
11516
+ ...updateArg,
11517
+ revision: finalRevision,
11518
+ streamEpoch: this.openCodeMessageStreamEpoch,
11519
+ } as IControllerReasoningUpdate | IControllerTextUpdate;
11520
+ const snapshotEvent: IControllerEvent = typeArg === 'reasoning'
11521
+ ? {
11522
+ type: 'session.reasoning.updated',
11523
+ harnessId: 'opencode',
11524
+ projectId,
11525
+ sessionId: update.sessionId,
11526
+ reasoningUpdate: update as IControllerReasoningUpdate,
11527
+ timestamp: update.sourceUpdatedAt,
11528
+ }
11529
+ : {
11530
+ type: 'session.text.updated',
11531
+ harnessId: 'opencode',
11532
+ projectId,
11533
+ sessionId: update.sessionId,
11534
+ textUpdate: update as IControllerTextUpdate,
11535
+ timestamp: update.sourceUpdatedAt,
11536
+ };
11537
+ const snapshotBytes = Buffer.byteLength(JSON.stringify(snapshotEvent), 'utf8');
11538
+ if (snapshotBytes > controllerMaxLiveMessageEventBytes) {
11539
+ this.advanceOpenCodeMessageStreamEpoch(true);
11540
+ return;
11541
+ }
11542
+
11543
+ const events: IControllerEvent[] = [];
11544
+ let baseTextUtf8Bytes = previousEntry.textUtf8Bytes;
11545
+ for (const [index, fragment] of fragments.entries()) {
11546
+ const fragmentTextUtf8Bytes = index === 0
11547
+ ? controllerTextAfterAppendUtf8Bytes(
11548
+ previous.text,
11549
+ baseTextUtf8Bytes,
11550
+ fragment.delta,
11551
+ )
11552
+ : baseTextUtf8Bytes + fragment.utf8Bytes;
11553
+ if (
11554
+ fragmentTextUtf8Bytes === undefined
11555
+ || !Number.isSafeInteger(fragmentTextUtf8Bytes)
11556
+ || fragmentTextUtf8Bytes <= baseTextUtf8Bytes
11557
+ ) {
11558
+ this.advanceOpenCodeMessageStreamEpoch(true);
11559
+ return;
11560
+ }
11561
+ const revision = this.openCodeMessageStreamRevision + index + 1;
11562
+ const commonDelta = {
11563
+ sessionId: update.sessionId,
11564
+ messageId: update.messageId,
11565
+ partId: update.partId,
11566
+ delta: fragment.delta,
11567
+ baseTextUtf8Bytes,
11568
+ textUtf8Bytes: fragmentTextUtf8Bytes,
11569
+ sourceUpdatedAt: update.sourceUpdatedAt,
11570
+ revision,
11571
+ streamEpoch: this.openCodeMessageStreamEpoch,
11572
+ };
11573
+ const event: IControllerEvent = typeArg === 'reasoning'
11574
+ ? {
11575
+ type: 'session.reasoning.delta',
11576
+ harnessId: 'opencode',
11577
+ projectId,
11578
+ sessionId: update.sessionId,
11579
+ reasoningDelta: commonDelta,
11580
+ timestamp: update.sourceUpdatedAt,
11581
+ }
11582
+ : {
11583
+ type: 'session.text.delta',
11584
+ harnessId: 'opencode',
11585
+ projectId,
11586
+ sessionId: update.sessionId,
11587
+ textDelta: commonDelta,
11588
+ timestamp: update.sourceUpdatedAt,
11589
+ };
11590
+ if (Buffer.byteLength(JSON.stringify(event), 'utf8') > controllerMaxLiveMessageEventBytes) {
11591
+ this.advanceOpenCodeMessageStreamEpoch(true);
11592
+ return;
11593
+ }
11594
+ events.push(event);
11595
+ baseTextUtf8Bytes = fragmentTextUtf8Bytes;
11596
+ }
11597
+ if (baseTextUtf8Bytes !== textUtf8Bytes) {
11598
+ this.advanceOpenCodeMessageStreamEpoch(true);
11599
+ return;
11600
+ }
11601
+
11602
+ this.openCodeMessageStreamRevision = finalRevision;
11603
+ cache.delete(key);
11604
+ cache.set(key, {
11605
+ projectId,
11606
+ update,
11607
+ textUtf8Bytes,
11608
+ bytes: snapshotBytes,
11609
+ } as ILatestReasoningUpdate & ILatestTextUpdate);
11610
+ const streamEpoch = this.openCodeMessageStreamEpoch;
11611
+ for (const event of events) {
11612
+ this.queueOpenCodeControllerEvent(event);
11613
+ if (this.openCodeMessageStreamEpoch !== streamEpoch) return;
11614
+ }
11615
+ return;
11616
+ }
11617
+
11146
11618
  this.openCodeMessageStreamRevision += 1;
11147
11619
  const update = {
11148
11620
  ...updateArg,
@@ -11166,12 +11638,18 @@ export class OpenCodeController {
11166
11638
  textUpdate: update as IControllerTextUpdate,
11167
11639
  timestamp: update.sourceUpdatedAt,
11168
11640
  };
11169
- if (Buffer.byteLength(JSON.stringify(event), 'utf8') > controllerMaxLiveMessageEventBytes) {
11641
+ const eventBytes = Buffer.byteLength(JSON.stringify(event), 'utf8');
11642
+ if (eventBytes > controllerMaxLiveMessageEventBytes) {
11170
11643
  this.advanceOpenCodeMessageStreamEpoch(true);
11171
11644
  return;
11172
11645
  }
11173
11646
  cache.delete(key);
11174
- cache.set(key, { projectId, update } as ILatestReasoningUpdate & ILatestTextUpdate);
11647
+ cache.set(key, {
11648
+ projectId,
11649
+ update,
11650
+ textUtf8Bytes: Buffer.byteLength(update.text, 'utf8'),
11651
+ bytes: eventBytes,
11652
+ } as ILatestReasoningUpdate & ILatestTextUpdate);
11175
11653
  this.queueOpenCodeControllerEvent(event);
11176
11654
  }
11177
11655
 
@@ -11218,6 +11696,7 @@ export class OpenCodeController {
11218
11696
  }
11219
11697
 
11220
11698
  private controllerEventKey(eventArg: IControllerEvent): string {
11699
+ const delta = controllerEventMessageDelta(eventArg);
11221
11700
  return JSON.stringify([
11222
11701
  eventArg.type,
11223
11702
  eventArg.harnessId ?? null,
@@ -11232,6 +11711,18 @@ export class OpenCodeController {
11232
11711
  eventArg.textUpdate
11233
11712
  ? controllerRuntimeIdKey(eventArg.textUpdate.partId)
11234
11713
  : null,
11714
+ delta
11715
+ ? [
11716
+ controllerRuntimeIdKey(delta.messageId),
11717
+ controllerRuntimeIdKey(delta.partId),
11718
+ delta.streamEpoch,
11719
+ delta.order?.messageIndex ?? null,
11720
+ delta.order?.partIndex ?? null,
11721
+ delta.baseTextUtf8Bytes,
11722
+ delta.textUtf8Bytes,
11723
+ delta.revision,
11724
+ ]
11725
+ : null,
11235
11726
  eventArg.toolExecution
11236
11727
  ? eventArg.toolExecution.status === 'completed' || eventArg.toolExecution.status === 'error'
11237
11728
  ? 'terminal'
@@ -11248,15 +11739,59 @@ export class OpenCodeController {
11248
11739
  ]);
11249
11740
  }
11250
11741
 
11251
- private queueOpenCodeControllerEvent(eventArg: IControllerEvent): void {
11252
- if (this.lifecycleState !== 'starting' && this.lifecycleState !== 'ready') return;
11742
+ private latestPendingControllerDelta(
11743
+ queueArg: Map<string, IControllerEvent>,
11744
+ eventArg: IControllerEvent,
11745
+ ): { key: string; event: IControllerEvent } | undefined {
11746
+ const pendingKey = controllerMessageDeltaPendingKey(eventArg);
11747
+ if (!pendingKey) return undefined;
11748
+ let tail: [string, IControllerEvent] | undefined;
11749
+ for (const entry of queueArg) tail = entry;
11750
+ if (!tail || controllerMessageDeltaPendingKey(tail[1]) !== pendingKey) return undefined;
11751
+ return { key: tail[0], event: tail[1] };
11752
+ }
11753
+
11754
+ private queueOpenCodeControllerEvent(eventArg: IControllerEvent): boolean {
11755
+ if (this.lifecycleState !== 'starting' && this.lifecycleState !== 'ready') return false;
11756
+ const delta = controllerEventMessageDelta(eventArg);
11757
+ if (delta) {
11758
+ if (Buffer.byteLength(JSON.stringify(eventArg), 'utf8') > controllerMaxLiveMessageEventBytes) {
11759
+ this.advanceOpenCodeMessageStreamEpoch(true);
11760
+ return false;
11761
+ }
11762
+ const pending = this.latestPendingControllerDelta(
11763
+ this.pendingOpenCodeControllerEvents,
11764
+ eventArg,
11765
+ );
11766
+ if (pending) {
11767
+ const merged = mergeControllerMessageDeltaEvents(pending.event, eventArg);
11768
+ if (!merged) {
11769
+ this.advanceOpenCodeMessageStreamEpoch(true);
11770
+ return false;
11771
+ }
11772
+ if (
11773
+ Buffer.byteLength(JSON.stringify(merged), 'utf8')
11774
+ <= controllerMaxLiveMessageEventBytes
11775
+ ) {
11776
+ this.pendingOpenCodeControllerEvents.set(pending.key, merged);
11777
+ this.startOpenCodeControllerEventDrain();
11778
+ return true;
11779
+ }
11780
+ }
11781
+ }
11253
11782
  const key = this.controllerEventKey(eventArg);
11254
- if (this.pendingOpenCodeControllerEvents.has(key)) {
11783
+ if (delta && this.pendingOpenCodeControllerEvents.has(key)) {
11784
+ this.advanceOpenCodeMessageStreamEpoch(true);
11785
+ return false;
11786
+ }
11787
+ if (!delta && this.pendingOpenCodeControllerEvents.has(key)) {
11255
11788
  this.pendingOpenCodeControllerEvents.delete(key);
11256
11789
  } else if (
11257
11790
  this.pendingOpenCodeControllerEvents.size >= maxPendingOpenCodeControllerEvents
11258
11791
  ) {
11259
- if (eventArg.reasoningUpdate || eventArg.textUpdate) {
11792
+ if (delta) {
11793
+ this.advanceOpenCodeStreams(true);
11794
+ } else if (eventArg.reasoningUpdate || eventArg.textUpdate) {
11260
11795
  this.advanceOpenCodeMessageStreamEpoch(true);
11261
11796
  } else if (eventArg.toolExecution) {
11262
11797
  this.advanceOpenCodeToolStreamEpoch(true);
@@ -11288,10 +11823,11 @@ export class OpenCodeController {
11288
11823
  rebasedEvent,
11289
11824
  );
11290
11825
  }
11291
- return;
11826
+ return false;
11292
11827
  }
11293
11828
  this.pendingOpenCodeControllerEvents.set(key, eventArg);
11294
11829
  this.startOpenCodeControllerEventDrain();
11830
+ return true;
11295
11831
  }
11296
11832
 
11297
11833
  private startOpenCodeControllerEventDrain(): void {
@@ -11358,7 +11894,11 @@ export class OpenCodeController {
11358
11894
  };
11359
11895
  this.setLatestFlexReasoningUpdate(
11360
11896
  this.openCodeToolExecutionKey(eventArg.projectId, update),
11361
- { projectId: eventArg.projectId, update },
11897
+ {
11898
+ projectId: eventArg.projectId,
11899
+ update,
11900
+ textUtf8Bytes: Buffer.byteLength(update.text, 'utf8'),
11901
+ },
11362
11902
  Buffer.byteLength(JSON.stringify(event), 'utf8'),
11363
11903
  );
11364
11904
  return event;
@@ -11377,7 +11917,11 @@ export class OpenCodeController {
11377
11917
  };
11378
11918
  this.setLatestFlexTextUpdate(
11379
11919
  this.openCodeToolExecutionKey(eventArg.projectId, update),
11380
- { projectId: eventArg.projectId, update },
11920
+ {
11921
+ projectId: eventArg.projectId,
11922
+ update,
11923
+ textUtf8Bytes: Buffer.byteLength(update.text, 'utf8'),
11924
+ },
11381
11925
  Buffer.byteLength(JSON.stringify(event), 'utf8'),
11382
11926
  );
11383
11927
  return event;
@@ -11385,10 +11929,51 @@ export class OpenCodeController {
11385
11929
  return eventArg;
11386
11930
  }
11387
11931
 
11388
- private queueFlexControllerEvent(eventArg: IControllerEvent): void {
11389
- if (this.lifecycleState !== 'starting' && this.lifecycleState !== 'ready') return;
11932
+ private queueFlexControllerEvent(eventArg: IControllerEvent): boolean {
11933
+ if (this.lifecycleState !== 'starting' && this.lifecycleState !== 'ready') return false;
11934
+ const delta = controllerEventMessageDelta(eventArg);
11935
+ if (delta) {
11936
+ if (Buffer.byteLength(JSON.stringify(eventArg), 'utf8') > controllerMaxLiveMessageEventBytes) {
11937
+ this.advanceFlexMessageStreamEpoch(true);
11938
+ return false;
11939
+ }
11940
+ const pending = this.latestPendingControllerDelta(
11941
+ this.pendingFlexControllerEvents,
11942
+ eventArg,
11943
+ );
11944
+ if (pending) {
11945
+ const merged = mergeControllerMessageDeltaEvents(pending.event, eventArg);
11946
+ if (!merged) {
11947
+ this.advanceFlexMessageStreamEpoch(true);
11948
+ return false;
11949
+ }
11950
+ const mergedBytes = this.flexControllerEventLiveBytes(merged);
11951
+ if (
11952
+ mergedBytes.message <= controllerMaxLiveMessageEventBytes
11953
+ ) {
11954
+ const pendingBytes = this.flexControllerEventLiveBytes(pending.event);
11955
+ if (
11956
+ this.latestFlexMessageUpdateBytes
11957
+ + this.pendingFlexMessageEventBytes
11958
+ - pendingBytes.message
11959
+ + mergedBytes.message
11960
+ > maxLatestFlexMessageUpdateBytes
11961
+ ) {
11962
+ this.advanceFlexMessageStreamEpoch(true);
11963
+ return false;
11964
+ }
11965
+ this.setPendingFlexControllerEvent(pending.key, merged);
11966
+ this.startFlexControllerEventDrain();
11967
+ return true;
11968
+ }
11969
+ }
11970
+ }
11390
11971
  const key = this.controllerEventKey(eventArg);
11391
- this.deletePendingFlexControllerEvent(key);
11972
+ if (delta && this.pendingFlexControllerEvents.has(key)) {
11973
+ this.advanceFlexMessageStreamEpoch(true);
11974
+ return false;
11975
+ }
11976
+ if (!delta) this.deletePendingFlexControllerEvent(key);
11392
11977
  const liveBytes = this.flexControllerEventLiveBytes(eventArg);
11393
11978
  const exceedsCountBound = this.pendingFlexControllerEvents.size >= maxPendingFlexControllerEvents;
11394
11979
  const exceedsPendingBound = exceedsCountBound
@@ -11403,7 +11988,12 @@ export class OpenCodeController {
11403
11988
  || this.pendingFlexControllerEvents.size > maxPendingFlexControllerEvents - 3;
11404
11989
  if (requiresCountCollapse) {
11405
11990
  this.advanceFlexStreams(true);
11406
- } else if (eventArg.reasoningUpdate || eventArg.textUpdate) {
11991
+ } else if (
11992
+ eventArg.reasoningUpdate
11993
+ || eventArg.reasoningDelta
11994
+ || eventArg.textUpdate
11995
+ || eventArg.textDelta
11996
+ ) {
11407
11997
  this.advanceFlexMessageStreamEpoch(true);
11408
11998
  } else if (eventArg.toolExecution) {
11409
11999
  this.advanceFlexToolStreamEpoch(true);
@@ -11421,10 +12011,11 @@ export class OpenCodeController {
11421
12011
  this.setPendingFlexControllerEvent(this.controllerEventKey(rebasedEvent), rebasedEvent);
11422
12012
  }
11423
12013
  this.startFlexControllerEventDrain();
11424
- return;
12014
+ return false;
11425
12015
  }
11426
12016
  this.setPendingFlexControllerEvent(key, eventArg);
11427
12017
  this.startFlexControllerEventDrain();
12018
+ return true;
11428
12019
  }
11429
12020
 
11430
12021
  private startFlexControllerEventDrain(): void {