@qwen-code/channel-base 0.23.4 → 0.24.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 (43) hide show
  1. package/dist/AcpBridge.d.ts.map +1 -1
  2. package/dist/AcpBridge.js +24 -6
  3. package/dist/AcpBridge.js.map +1 -1
  4. package/dist/ChannelAgentBridge.d.ts +11 -0
  5. package/dist/ChannelAgentBridge.d.ts.map +1 -1
  6. package/dist/ChannelAgentBridge.js +9 -0
  7. package/dist/ChannelAgentBridge.js.map +1 -1
  8. package/dist/ChannelBase.d.ts +1 -0
  9. package/dist/ChannelBase.d.ts.map +1 -1
  10. package/dist/ChannelBase.js +40 -5
  11. package/dist/ChannelBase.js.map +1 -1
  12. package/dist/DaemonChannelBridge.d.ts +1 -0
  13. package/dist/DaemonChannelBridge.d.ts.map +1 -1
  14. package/dist/DaemonChannelBridge.js +42 -3
  15. package/dist/DaemonChannelBridge.js.map +1 -1
  16. package/dist/SessionRouter.d.ts +2 -2
  17. package/dist/SessionRouter.d.ts.map +1 -1
  18. package/dist/SessionRouter.js +4 -2
  19. package/dist/SessionRouter.js.map +1 -1
  20. package/dist/background-output-coordinator.d.ts +66 -0
  21. package/dist/background-output-coordinator.d.ts.map +1 -0
  22. package/dist/background-output-coordinator.js +640 -0
  23. package/dist/background-output-coordinator.js.map +1 -0
  24. package/dist/index.d.ts +7 -2
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +4 -1
  27. package/dist/index.js.map +1 -1
  28. package/dist/named-session-manager.d.ts +2 -0
  29. package/dist/named-session-manager.d.ts.map +1 -1
  30. package/dist/named-session-manager.js +5 -2
  31. package/dist/named-session-manager.js.map +1 -1
  32. package/dist/output-mode.d.ts +5 -0
  33. package/dist/output-mode.d.ts.map +1 -0
  34. package/dist/output-mode.js +28 -0
  35. package/dist/output-mode.js.map +1 -0
  36. package/dist/output-turn.d.ts +27 -0
  37. package/dist/output-turn.d.ts.map +1 -0
  38. package/dist/output-turn.js +48 -0
  39. package/dist/output-turn.js.map +1 -0
  40. package/dist/types.d.ts +7 -1
  41. package/dist/types.d.ts.map +1 -1
  42. package/dist/types.js.map +1 -1
  43. package/package.json +1 -1
@@ -0,0 +1,640 @@
1
+ import { sanitizeLogText } from './sanitize.js';
2
+ const BACKGROUND_OUTPUT_TIMEOUT_MS = 10 * 60 * 1000;
3
+ const BACKGROUND_OUTPUT_RETRY_MS = 30 * 1000;
4
+ const BACKGROUND_OUTPUT_MAX_RETRIES = 3;
5
+ export class BackgroundOutputCoordinator {
6
+ options;
7
+ backgroundResponseAggregations = new Map();
8
+ detachedBackgroundResponseAggregations = new Set();
9
+ pendingBackgroundResponseTerminals = new Map();
10
+ detachedPendingBackgroundResponseTerminals = new Set();
11
+ constructor(options) {
12
+ this.options = options;
13
+ }
14
+ /** False leaves delivery to the adapter's immediate response path. */
15
+ async dispatch(sessionId, text, context) {
16
+ if ((this.options.outputMode !== 'per_turn' &&
17
+ this.options.outputMode !== 'per_task') ||
18
+ !context ||
19
+ typeof context.turnComplete !== 'boolean') {
20
+ return false;
21
+ }
22
+ await this.collect(sessionId, text, context);
23
+ return true;
24
+ }
25
+ async collect(sessionId, text, context) {
26
+ const target = this.options.getTarget(sessionId);
27
+ if (!target)
28
+ return;
29
+ const key = JSON.stringify([
30
+ sessionId,
31
+ context.kind,
32
+ context.taskId,
33
+ context.turnId,
34
+ ]);
35
+ let current = this.backgroundResponseAggregations.get(key);
36
+ let parked = this.pendingBackgroundResponseTerminals.get(key);
37
+ if (current?.turnComplete === true) {
38
+ this.detachedBackgroundResponseAggregations.add(current);
39
+ this.backgroundResponseAggregations.delete(key);
40
+ current = undefined;
41
+ }
42
+ if (!current &&
43
+ (parked?.turnEnded === true ||
44
+ (parked?.turnComplete === true &&
45
+ (parked.retryTimer || parked.retryInFlight || parked.resolvers > 0)))) {
46
+ if (!parked.turnEnded) {
47
+ this.detachedPendingBackgroundResponseTerminals.add(parked);
48
+ }
49
+ parked = {
50
+ sessionId,
51
+ target,
52
+ sourceLabel: this.options.getSourceLabel?.(sessionId),
53
+ resolvers: 0,
54
+ held: [],
55
+ };
56
+ this.pendingBackgroundResponseTerminals.set(key, parked);
57
+ }
58
+ if (!current && text.trim().length === 0) {
59
+ // The first segment's target resolution can suspend (named-session owner
60
+ // lock), so a turn's terminal marker may arrive before the aggregation
61
+ // exists. Park it instead of routing it to the empty-text early return,
62
+ // or the completed turn only surfaces via the bounded wait, mislabeled.
63
+ if (!parked ||
64
+ (parked.resolvers === 0 &&
65
+ !parked.retryTimer &&
66
+ !parked.retryInFlight &&
67
+ !parked.resolutionDropped &&
68
+ !parked.turnComplete)) {
69
+ if (parked)
70
+ this.pendingBackgroundResponseTerminals.delete(key);
71
+ return;
72
+ }
73
+ if (context.turnComplete) {
74
+ parked.turnComplete = true;
75
+ parked.status = context.status;
76
+ parked.label = context.label ?? parked.label;
77
+ parked.completionPartial = context.partial === true;
78
+ if (parked.resolvers === 0 &&
79
+ !parked.retryTimer &&
80
+ !parked.retryInFlight &&
81
+ (parked.retryAttempts ?? 0) >= BACKGROUND_OUTPUT_MAX_RETRIES) {
82
+ parked.turnEnded = true;
83
+ this.pendingBackgroundResponseTerminals.delete(key);
84
+ }
85
+ }
86
+ return;
87
+ }
88
+ if (!current) {
89
+ parked ??= {
90
+ sessionId,
91
+ target,
92
+ sourceLabel: this.options.getSourceLabel?.(sessionId),
93
+ resolvers: 0,
94
+ held: [],
95
+ };
96
+ this.pendingBackgroundResponseTerminals.set(key, parked);
97
+ this.holdPendingBackgroundResponse(parked, text, context);
98
+ parked.resolvers++;
99
+ try {
100
+ let delivery;
101
+ try {
102
+ delivery = await this.options.resolveDelivery(sessionId);
103
+ }
104
+ catch (error) {
105
+ if (parked.resolvers === 1 &&
106
+ !this.backgroundResponseAggregations.has(key)) {
107
+ this.scheduleBackgroundResponseResolutionRetry(key, sessionId, parked);
108
+ }
109
+ else {
110
+ const existing = this.backgroundResponseAggregations.get(key);
111
+ if (existing) {
112
+ if (parked.held.length > 0) {
113
+ this.applyHeldBackgroundResponses(existing, parked);
114
+ }
115
+ this.applyPendingBackgroundResponseTerminal(existing, parked);
116
+ if (parked.resolvers === 1) {
117
+ if (existing.turnComplete) {
118
+ await this.completeBackgroundResponseAggregation(key, existing);
119
+ }
120
+ else {
121
+ this.scheduleBackgroundResponseAggregationFlush(key, existing);
122
+ }
123
+ }
124
+ }
125
+ }
126
+ throw error;
127
+ }
128
+ if (!delivery ||
129
+ this.options.getTarget(sessionId) !== delivery.target) {
130
+ if (parked.resolvers === 1 &&
131
+ !this.backgroundResponseAggregations.has(key)) {
132
+ this.scheduleBackgroundResponseResolutionRetry(key, sessionId, parked);
133
+ }
134
+ else {
135
+ const existing = this.backgroundResponseAggregations.get(key);
136
+ if (existing) {
137
+ if (parked.held.length > 0) {
138
+ this.applyHeldBackgroundResponses(existing, parked);
139
+ }
140
+ this.applyPendingBackgroundResponseTerminal(existing, parked);
141
+ if (parked.resolvers === 1) {
142
+ if (existing.turnComplete) {
143
+ await this.completeBackgroundResponseAggregation(key, existing);
144
+ }
145
+ else {
146
+ this.scheduleBackgroundResponseAggregationFlush(key, existing);
147
+ }
148
+ }
149
+ }
150
+ }
151
+ return;
152
+ }
153
+ if (parked.retiring ||
154
+ parked.turnEnded === true ||
155
+ this.pendingBackgroundResponseTerminals.get(key) !== parked) {
156
+ await this.flushDetachedBackgroundResponse(key, sessionId, parked, delivery);
157
+ return;
158
+ }
159
+ parked.sourceLabel = delivery.sourceLabel;
160
+ if (parked.retryTimer) {
161
+ clearTimeout(parked.retryTimer);
162
+ parked.retryTimer = undefined;
163
+ }
164
+ current =
165
+ this.backgroundResponseAggregations.get(key) ??
166
+ this.createBackgroundResponseAggregation(key, sessionId, parked.held[0]?.context ?? context, delivery.target, delivery.sourceLabel);
167
+ this.applyHeldBackgroundResponses(current, parked);
168
+ this.applyPendingBackgroundResponseTerminal(current, parked);
169
+ if (parked.resolutionDropped) {
170
+ current.resolutionDropped = true;
171
+ parked.resolutionDropped = undefined;
172
+ }
173
+ }
174
+ finally {
175
+ parked.resolvers--;
176
+ if (this.pendingBackgroundResponseTerminals.get(key) === parked &&
177
+ parked.resolvers === 0 &&
178
+ !parked.retryTimer &&
179
+ parked.held.length === 0 &&
180
+ (!parked.resolutionDropped || parked.turnEnded)) {
181
+ this.pendingBackgroundResponseTerminals.delete(key);
182
+ }
183
+ }
184
+ if (!current)
185
+ return;
186
+ if (current.turnComplete && parked.resolvers > 0)
187
+ return;
188
+ if (!current.turnComplete) {
189
+ this.scheduleBackgroundResponseAggregationFlush(key, current);
190
+ }
191
+ else {
192
+ await this.completeBackgroundResponseAggregation(key, current);
193
+ }
194
+ return;
195
+ }
196
+ current.status = context.status;
197
+ current.label = context.label ?? current.label;
198
+ if (text.trim())
199
+ current.text = text;
200
+ if (context.turnComplete && parked && parked.resolvers > 0) {
201
+ parked.turnComplete = true;
202
+ parked.status = context.status;
203
+ parked.label = context.label ?? parked.label;
204
+ parked.completionPartial = context.partial === true;
205
+ }
206
+ else if (context.turnComplete) {
207
+ current.turnComplete = true;
208
+ current.completionPartial = context.partial === true;
209
+ }
210
+ else if (parked?.turnComplete && parked.resolvers === 0) {
211
+ current.turnComplete = true;
212
+ current.status = parked.status ?? current.status;
213
+ current.label = parked.label ?? current.label;
214
+ current.completionPartial = parked.completionPartial === true;
215
+ }
216
+ current.resolutionDropped ||= parked?.resolutionDropped;
217
+ if (!current.turnComplete) {
218
+ this.scheduleBackgroundResponseAggregationFlush(key, current);
219
+ return;
220
+ }
221
+ await this.completeBackgroundResponseAggregation(key, current);
222
+ }
223
+ flushBackgroundResponseAggregation(key, aggregation) {
224
+ if (aggregation.flushing)
225
+ return Promise.resolve();
226
+ const flushing = this.flushBackgroundResponseAggregationInner(key, aggregation).finally(() => {
227
+ if (aggregation.flushing === flushing)
228
+ aggregation.flushing = undefined;
229
+ });
230
+ aggregation.flushing = flushing;
231
+ return flushing;
232
+ }
233
+ async flushBackgroundResponseAggregationInner(key, aggregation) {
234
+ if (this.backgroundResponseAggregations.get(key) !== aggregation &&
235
+ !this.detachedBackgroundResponseAggregations.has(aggregation)) {
236
+ return;
237
+ }
238
+ if (aggregation.retryTimer)
239
+ clearTimeout(aggregation.retryTimer);
240
+ aggregation.retryTimer = undefined;
241
+ let delivery = aggregation.delivery;
242
+ if (!delivery) {
243
+ if (!aggregation.text) {
244
+ // A turn whose text was already drained by the bounded wait still owes
245
+ // the user its completion: the last delivery was marked partial.
246
+ if (!this.owesTerminalBackgroundResponse(aggregation)) {
247
+ if (aggregation.retiring || aggregation.turnComplete) {
248
+ this.removeBackgroundResponseAggregation(key, aggregation);
249
+ }
250
+ else {
251
+ this.scheduleBackgroundResponseAggregationFlush(key, aggregation);
252
+ }
253
+ return;
254
+ }
255
+ }
256
+ if (aggregation.timeoutTimer)
257
+ clearTimeout(aggregation.timeoutTimer);
258
+ aggregation.timeoutTimer = undefined;
259
+ const text = aggregation.text;
260
+ aggregation.text = '';
261
+ delivery = {
262
+ status: aggregation.status,
263
+ kind: aggregation.kind,
264
+ label: aggregation.label,
265
+ text,
266
+ partial: this.isPartialBackgroundResponseDelivery(aggregation, text.length > 0),
267
+ attempts: 0,
268
+ send: this.options.createDelivery(aggregation.sessionId, {
269
+ target: aggregation.target,
270
+ sourceLabel: aggregation.sourceLabel,
271
+ }),
272
+ };
273
+ aggregation.delivery = delivery;
274
+ }
275
+ let error;
276
+ let composedTurnComplete = false;
277
+ try {
278
+ const result = await delivery.send({
279
+ status: delivery.status,
280
+ kind: delivery.kind,
281
+ label: delivery.label,
282
+ text: delivery.text,
283
+ partial: delivery.partial,
284
+ turnComplete: aggregation.turnComplete === true,
285
+ });
286
+ composedTurnComplete = result.turnComplete;
287
+ }
288
+ catch (caught) {
289
+ error = caught;
290
+ }
291
+ if (error === undefined) {
292
+ aggregation.delivery = undefined;
293
+ aggregation.delivered = true;
294
+ if (composedTurnComplete &&
295
+ aggregation.turnComplete &&
296
+ delivery.partial !== true &&
297
+ !aggregation.retiring) {
298
+ aggregation.completionDelivered = true;
299
+ }
300
+ if (aggregation.retiring || aggregation.turnComplete) {
301
+ await this.flushBackgroundResponseAggregationInner(key, aggregation);
302
+ }
303
+ else {
304
+ // The turn is still open: keep the entry so its later segments re-join
305
+ // this one (and stay partial), and re-arm the bounded wait
306
+ // so a silent turn is still reaped.
307
+ this.scheduleBackgroundResponseAggregationFlush(key, aggregation);
308
+ }
309
+ return;
310
+ }
311
+ delivery.attempts++;
312
+ this.options.log(`background response delivery failed (attempt ${delivery.attempts}): ${sanitizeLogText(error instanceof Error ? error.message : String(error), 300)}\n`);
313
+ if (aggregation.retiring ||
314
+ delivery.attempts >= BACKGROUND_OUTPUT_MAX_RETRIES ||
315
+ // A permanently rejected send cannot
316
+ // succeed later; retrying it only spends the chat's send quota.
317
+ !this.options.isRetryableError(error)) {
318
+ aggregation.delivery = undefined;
319
+ aggregation.dropped = true;
320
+ if (!aggregation.text) {
321
+ if (aggregation.retiring || aggregation.turnComplete) {
322
+ this.removeBackgroundResponseAggregation(key, aggregation);
323
+ }
324
+ else {
325
+ this.scheduleBackgroundResponseAggregationFlush(key, aggregation);
326
+ }
327
+ }
328
+ else if (aggregation.retiring || aggregation.turnComplete) {
329
+ await this.flushBackgroundResponseAggregationInner(key, aggregation);
330
+ }
331
+ else {
332
+ this.scheduleBackgroundResponseAggregationFlush(key, aggregation);
333
+ }
334
+ return;
335
+ }
336
+ aggregation.retryTimer = setTimeout(() => {
337
+ aggregation.retryTimer = undefined;
338
+ void this.flushBackgroundResponseAggregation(key, aggregation);
339
+ }, BACKGROUND_OUTPUT_RETRY_MS);
340
+ aggregation.retryTimer.unref?.();
341
+ }
342
+ /**
343
+ * A delivery is partial whenever it is not the turn's whole output: the
344
+ * turn is still open, earlier text already went out (or was given up on),
345
+ * or the turn itself ended early.
346
+ */
347
+ isPartialBackgroundResponseDelivery(aggregation, hasText) {
348
+ return (hasText &&
349
+ (aggregation.delivered === true ||
350
+ aggregation.dropped === true ||
351
+ aggregation.resolutionDropped === true ||
352
+ aggregation.retiring === true ||
353
+ aggregation.completionPartial === true ||
354
+ aggregation.turnComplete !== true));
355
+ }
356
+ /**
357
+ * If a turn outlives the bounded wait, its final empty marker still needs
358
+ * a terminal delivery so recipients learn that the partial output finished.
359
+ */
360
+ owesTerminalBackgroundResponse(aggregation) {
361
+ return (aggregation.turnComplete === true &&
362
+ aggregation.delivered === true &&
363
+ aggregation.completionDelivered !== true &&
364
+ aggregation.retiring !== true &&
365
+ aggregation.completionPartial !== true &&
366
+ aggregation.dropped !== true &&
367
+ aggregation.resolutionDropped !== true);
368
+ }
369
+ removeBackgroundResponseAggregation(key, aggregation) {
370
+ if (this.backgroundResponseAggregations.get(key) === aggregation) {
371
+ this.backgroundResponseAggregations.delete(key);
372
+ }
373
+ this.detachedBackgroundResponseAggregations.delete(aggregation);
374
+ }
375
+ drain(sessionId) {
376
+ const flushes = [];
377
+ for (const [key, pending] of this.pendingBackgroundResponseTerminals) {
378
+ if (sessionId !== undefined && pending.sessionId !== sessionId)
379
+ continue;
380
+ if (pending.retryTimer)
381
+ clearTimeout(pending.retryTimer);
382
+ pending.retryTimer = undefined;
383
+ pending.retiring = true;
384
+ pending.turnComplete = true;
385
+ pending.completionPartial = true;
386
+ this.pendingBackgroundResponseTerminals.delete(key);
387
+ this.detachedPendingBackgroundResponseTerminals.add(pending);
388
+ }
389
+ for (const pending of this.detachedPendingBackgroundResponseTerminals) {
390
+ if (sessionId !== undefined && pending.sessionId !== sessionId)
391
+ continue;
392
+ if (pending.retryTimer)
393
+ clearTimeout(pending.retryTimer);
394
+ pending.retryTimer = undefined;
395
+ pending.retiring = true;
396
+ pending.turnComplete = true;
397
+ pending.completionPartial = true;
398
+ if (pending.held.length > 0 &&
399
+ this.options.getTarget(pending.sessionId) === pending.target) {
400
+ flushes.push(this.flushDetachedBackgroundResponse('', pending.sessionId, pending, {
401
+ target: pending.target,
402
+ sourceLabel: pending.sourceLabel,
403
+ }));
404
+ }
405
+ else if (pending.held.length > 0) {
406
+ this.options.log(`background response target unavailable during drain; ${pending.held.length} buffered segment(s) discarded\n`);
407
+ pending.held.length = 0;
408
+ this.detachedPendingBackgroundResponseTerminals.delete(pending);
409
+ }
410
+ else if (pending.held.length === 0) {
411
+ this.detachedPendingBackgroundResponseTerminals.delete(pending);
412
+ }
413
+ }
414
+ const aggregations = new Set([
415
+ ...this.backgroundResponseAggregations.values(),
416
+ ...this.detachedBackgroundResponseAggregations,
417
+ ]);
418
+ for (const aggregation of aggregations) {
419
+ if (sessionId !== undefined && aggregation.sessionId !== sessionId) {
420
+ continue;
421
+ }
422
+ if (aggregation.timeoutTimer)
423
+ clearTimeout(aggregation.timeoutTimer);
424
+ if (aggregation.retryTimer)
425
+ clearTimeout(aggregation.retryTimer);
426
+ aggregation.timeoutTimer = undefined;
427
+ aggregation.retryTimer = undefined;
428
+ aggregation.retiring = true;
429
+ aggregation.turnComplete = true;
430
+ aggregation.completionPartial = true;
431
+ flushes.push(aggregation.flushing ??
432
+ this.flushBackgroundResponseAggregation(aggregation.key, aggregation));
433
+ }
434
+ return Promise.allSettled(flushes).then((results) => {
435
+ for (const result of results) {
436
+ if (result.status === 'rejected') {
437
+ const error = result.reason;
438
+ this.options.log(`background response delivery failed during drain: ${sanitizeLogText(error instanceof Error ? error.message : String(error), 300)}\n`);
439
+ }
440
+ }
441
+ });
442
+ }
443
+ createBackgroundResponseAggregation(key, sessionId, context, target, sourceLabel) {
444
+ const aggregation = {
445
+ key,
446
+ sessionId,
447
+ target,
448
+ sourceLabel,
449
+ status: context.status,
450
+ kind: context.kind,
451
+ label: context.label,
452
+ text: '',
453
+ };
454
+ this.backgroundResponseAggregations.set(key, aggregation);
455
+ return aggregation;
456
+ }
457
+ scheduleBackgroundResponseResolutionRetry(key, sessionId, pending) {
458
+ if (pending.retiring)
459
+ return;
460
+ if (pending.retryTimer)
461
+ return;
462
+ pending.retryAttempts = (pending.retryAttempts ?? 0) + 1;
463
+ if (pending.retryAttempts >= BACKGROUND_OUTPUT_MAX_RETRIES) {
464
+ this.options.log(`background response target unresolved after ${pending.retryAttempts} attempts; ${pending.held.length} buffered segment(s) discarded\n`);
465
+ pending.resolutionDropped = true;
466
+ pending.turnEnded = pending.turnComplete === true;
467
+ pending.held.length = 0;
468
+ pending.turnComplete = undefined;
469
+ pending.status = undefined;
470
+ pending.label = undefined;
471
+ pending.completionPartial = undefined;
472
+ if (pending.turnEnded) {
473
+ this.detachedPendingBackgroundResponseTerminals.delete(pending);
474
+ }
475
+ return;
476
+ }
477
+ pending.retryTimer = setTimeout(() => {
478
+ pending.retryTimer = undefined;
479
+ pending.retryInFlight = true;
480
+ void this.retryBackgroundResponseResolution(key, sessionId, pending)
481
+ .catch((error) => {
482
+ this.options.log(`background response target resolution failed: ${sanitizeLogText(error instanceof Error ? error.message : String(error), 300)}\n`);
483
+ })
484
+ .finally(() => {
485
+ pending.retryInFlight = false;
486
+ if (pending.retiring ||
487
+ (!pending.retryTimer && !pending.resolutionDropped)) {
488
+ this.detachedPendingBackgroundResponseTerminals.delete(pending);
489
+ }
490
+ if (this.pendingBackgroundResponseTerminals.get(key) === pending &&
491
+ pending.resolvers === 0 &&
492
+ !pending.retryTimer &&
493
+ pending.held.length === 0 &&
494
+ (!pending.resolutionDropped || pending.turnEnded)) {
495
+ this.pendingBackgroundResponseTerminals.delete(key);
496
+ }
497
+ });
498
+ }, BACKGROUND_OUTPUT_RETRY_MS);
499
+ pending.retryTimer.unref?.();
500
+ const active = this.pendingBackgroundResponseTerminals.get(key);
501
+ if (!active || active === pending) {
502
+ this.pendingBackgroundResponseTerminals.set(key, pending);
503
+ }
504
+ }
505
+ async retryBackgroundResponseResolution(key, sessionId, pending) {
506
+ let delivery;
507
+ try {
508
+ delivery = await this.options.resolveDelivery(sessionId);
509
+ }
510
+ catch (error) {
511
+ if ((this.pendingBackgroundResponseTerminals.get(key) === pending &&
512
+ !this.backgroundResponseAggregations.has(key)) ||
513
+ this.detachedPendingBackgroundResponseTerminals.has(pending)) {
514
+ this.scheduleBackgroundResponseResolutionRetry(key, sessionId, pending);
515
+ }
516
+ throw error;
517
+ }
518
+ if (!delivery || this.options.getTarget(sessionId) !== delivery.target) {
519
+ if (pending.retiring) {
520
+ if (pending.held.length > 0) {
521
+ this.options.log(`background response target unavailable during drain; ${pending.held.length} buffered segment(s) discarded\n`);
522
+ }
523
+ pending.held.length = 0;
524
+ this.detachedPendingBackgroundResponseTerminals.delete(pending);
525
+ return;
526
+ }
527
+ if ((this.pendingBackgroundResponseTerminals.get(key) === pending &&
528
+ !this.backgroundResponseAggregations.has(key)) ||
529
+ this.detachedPendingBackgroundResponseTerminals.has(pending)) {
530
+ this.scheduleBackgroundResponseResolutionRetry(key, sessionId, pending);
531
+ }
532
+ return;
533
+ }
534
+ if (pending.retiring ||
535
+ this.pendingBackgroundResponseTerminals.get(key) !== pending ||
536
+ pending.turnComplete) {
537
+ await this.flushDetachedBackgroundResponse(key, sessionId, pending, delivery);
538
+ return;
539
+ }
540
+ const first = pending.held[0];
541
+ if (!first)
542
+ return;
543
+ const aggregation = this.createBackgroundResponseAggregation(key, sessionId, first.context, delivery.target, delivery.sourceLabel);
544
+ this.applyHeldBackgroundResponses(aggregation, pending);
545
+ if (pending.resolutionDropped) {
546
+ aggregation.resolutionDropped = true;
547
+ pending.resolutionDropped = undefined;
548
+ }
549
+ if (this.pendingBackgroundResponseTerminals.get(key) === pending) {
550
+ this.pendingBackgroundResponseTerminals.delete(key);
551
+ }
552
+ this.scheduleBackgroundResponseAggregationFlush(key, aggregation);
553
+ }
554
+ holdPendingBackgroundResponse(pending, text, context) {
555
+ if (text.trim().length > 0)
556
+ pending.held.push({ text, context });
557
+ if (context.turnComplete) {
558
+ pending.turnComplete = true;
559
+ pending.status = context.status;
560
+ pending.label = context.label ?? pending.label;
561
+ pending.completionPartial = context.partial === true;
562
+ }
563
+ }
564
+ applyHeldBackgroundResponses(aggregation, pending) {
565
+ for (const { text, context } of pending.held.splice(0)) {
566
+ aggregation.status = context.status;
567
+ aggregation.label = context.label ?? aggregation.label;
568
+ if (text.trim())
569
+ aggregation.text = text;
570
+ if (context.turnComplete) {
571
+ aggregation.turnComplete = true;
572
+ aggregation.completionPartial = context.partial === true;
573
+ }
574
+ }
575
+ }
576
+ applyPendingBackgroundResponseTerminal(aggregation, pending) {
577
+ if (!pending.turnComplete)
578
+ return;
579
+ aggregation.turnComplete = true;
580
+ aggregation.status = pending.status ?? aggregation.status;
581
+ aggregation.label = pending.label ?? aggregation.label;
582
+ aggregation.completionPartial = pending.completionPartial === true;
583
+ pending.turnComplete = undefined;
584
+ pending.status = undefined;
585
+ pending.label = undefined;
586
+ pending.completionPartial = undefined;
587
+ }
588
+ async completeBackgroundResponseAggregation(key, aggregation) {
589
+ if (aggregation.delivery) {
590
+ aggregation.delivery.status = aggregation.status;
591
+ aggregation.delivery.label =
592
+ aggregation.label ?? aggregation.delivery.label;
593
+ aggregation.delivery.partial =
594
+ this.isPartialBackgroundResponseDelivery(aggregation, aggregation.delivery.text.length > 0) || aggregation.text.length > 0;
595
+ }
596
+ if (aggregation.timeoutTimer)
597
+ clearTimeout(aggregation.timeoutTimer);
598
+ aggregation.timeoutTimer = undefined;
599
+ await this.flushBackgroundResponseAggregation(key, aggregation);
600
+ }
601
+ async flushDetachedBackgroundResponse(key, sessionId, pending, delivery) {
602
+ const first = pending.held[0];
603
+ if (!first) {
604
+ this.detachedPendingBackgroundResponseTerminals.delete(pending);
605
+ return;
606
+ }
607
+ const aggregation = {
608
+ key,
609
+ sessionId,
610
+ target: delivery.target,
611
+ sourceLabel: delivery.sourceLabel,
612
+ status: first.context.status,
613
+ kind: first.context.kind,
614
+ label: first.context.label,
615
+ text: '',
616
+ turnComplete: pending.turnComplete,
617
+ completionPartial: pending.completionPartial,
618
+ resolutionDropped: pending.resolutionDropped,
619
+ };
620
+ this.applyHeldBackgroundResponses(aggregation, pending);
621
+ aggregation.status = pending.status ?? aggregation.status;
622
+ aggregation.label = pending.label ?? aggregation.label;
623
+ if (this.pendingBackgroundResponseTerminals.get(key) === pending) {
624
+ this.pendingBackgroundResponseTerminals.delete(key);
625
+ }
626
+ this.detachedPendingBackgroundResponseTerminals.delete(pending);
627
+ this.detachedBackgroundResponseAggregations.add(aggregation);
628
+ await this.flushBackgroundResponseAggregation(key, aggregation);
629
+ }
630
+ scheduleBackgroundResponseAggregationFlush(key, aggregation) {
631
+ if (aggregation.timeoutTimer || aggregation.delivery)
632
+ return;
633
+ aggregation.timeoutTimer = setTimeout(() => {
634
+ aggregation.timeoutTimer = undefined;
635
+ void this.flushBackgroundResponseAggregation(key, aggregation);
636
+ }, BACKGROUND_OUTPUT_TIMEOUT_MS);
637
+ aggregation.timeoutTimer.unref?.();
638
+ }
639
+ }
640
+ //# sourceMappingURL=background-output-coordinator.js.map