@dsh-enhanced/assistant-delivery 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/service.js CHANGED
@@ -51,6 +51,8 @@ function pickerIncludesRoute(picker, route) {
51
51
  const model = picker.models.find(candidate => candidate.provider === route.provider && candidate.id === route.model);
52
52
  return model !== undefined && (route.reasoningEffort === undefined || model.effortIds.includes(route.reasoningEffort));
53
53
  }
54
+ const MODEL_SELECTION_WAIT_POLL_MS = 500;
55
+ const MODEL_SELECTION_WAIT_TIMEOUT_MS = 120_000;
54
56
  function isModelSelectionPayload(value) {
55
57
  if (value === null || typeof value !== 'object')
56
58
  return false;
@@ -58,6 +60,7 @@ function isModelSelectionPayload(value) {
58
60
  const principal = payload.principal;
59
61
  return typeof payload.callbackEventId === 'string'
60
62
  && typeof payload.callbackChatId === 'string'
63
+ && typeof payload.cardMessageId === 'string'
61
64
  && typeof payload.bindingId === 'string'
62
65
  && principal !== undefined
63
66
  && typeof principal.channel === 'string'
@@ -173,6 +176,7 @@ export class AssistantDeliveryService extends Service {
173
176
  conversationTransitions = new Map();
174
177
  agentApprovalBindings = new WeakMap();
175
178
  toolApprovalControllers = new Set();
179
+ modelSelectionWaiters = new Map();
176
180
  modelSelectionFlight;
177
181
  modelSelectionRetryTimer;
178
182
  runtime;
@@ -232,6 +236,7 @@ export class AssistantDeliveryService extends Service {
232
236
  modelPickerTtlMs: config.modelPickerTtlMs,
233
237
  permissionPickerTtlMs: config.permissionPickerTtlMs,
234
238
  getModelSelection: conversation => this.deliveryStore.getModelSelection(conversation),
239
+ clearStaleModelReasoningEffort: (conversation, expected) => this.deliveryStore.clearStaleModelReasoningEffort({ conversation, expected }),
235
240
  imageMaterializer,
236
241
  isInboundAuthorized: (binding, envelope) => this.isInboundAuthorized(binding, envelope),
237
242
  isPermissionController: (binding, envelope) => this.isPermissionController(binding, envelope),
@@ -277,6 +282,7 @@ export class AssistantDeliveryService extends Service {
277
282
  this.start();
278
283
  ctx.effect(() => async () => {
279
284
  this.active = false;
285
+ this.cancelModelSelectionWaiters();
280
286
  for (const controller of this.toolApprovalControllers) {
281
287
  controller.abort(new Error('assistant-delivery is stopping'));
282
288
  }
@@ -439,13 +445,18 @@ export class AssistantDeliveryService extends Service {
439
445
  || JSON.stringify(binding.principal) !== JSON.stringify(current.principal)) {
440
446
  throw new AssistantDeliveryError('missing-binding', 'model callback principal or chat does not own the active binding');
441
447
  }
442
- const picker = this.deliveryStore.getModelPicker(input.operationId, input.bindingId);
448
+ const record = this.deliveryStore.getModelPickerRecord(input.operationId, input.bindingId);
443
449
  const expected = {
444
450
  revision: input.expectedRevision,
445
451
  provider: input.modelProvider,
446
452
  model: input.model,
447
453
  ...(input.reasoningEffort === undefined ? {} : { reasoningEffort: input.reasoningEffort }),
448
454
  };
455
+ if (record === undefined || !['accepted', 'delivered', 'read'].includes(record.status)
456
+ || record.providerMessageId !== input.cardMessageId) {
457
+ throw new AssistantDeliveryError('missing-binding', 'model picker confirmation is unavailable or invalid');
458
+ }
459
+ const picker = record.intent.modelPicker;
449
460
  if (picker === undefined || !pickerIncludesRoute(picker, expected)) {
450
461
  throw new AssistantDeliveryError('missing-binding', 'model picker confirmation is unavailable or invalid');
451
462
  }
@@ -457,6 +468,7 @@ export class AssistantDeliveryService extends Service {
457
468
  const payload = {
458
469
  callbackEventId: input.callbackEventId,
459
470
  callbackChatId: input.callbackChatId,
471
+ cardMessageId: input.cardMessageId,
460
472
  bindingId: input.bindingId,
461
473
  principal: current.principal,
462
474
  provider: input.provider,
@@ -477,6 +489,112 @@ export class AssistantDeliveryService extends Service {
477
489
  void this.drainModelSelectionSettlements();
478
490
  return { status: 'pending' };
479
491
  }
492
+ async awaitModelSelection(input, signal) {
493
+ this.assertActive();
494
+ const expected = {
495
+ revision: input.expectedRevision,
496
+ provider: input.modelProvider,
497
+ model: input.model,
498
+ ...(input.reasoningEffort === undefined ? {} : { reasoningEffort: input.reasoningEffort }),
499
+ };
500
+ const payload = {
501
+ callbackEventId: input.callbackEventId,
502
+ callbackChatId: input.callbackChatId,
503
+ cardMessageId: input.cardMessageId,
504
+ bindingId: input.bindingId,
505
+ principal: input.principal,
506
+ provider: input.provider,
507
+ modelProvider: input.modelProvider,
508
+ model: input.model,
509
+ reasoningEffort: input.reasoningEffort ?? null,
510
+ expectedRevision: input.expectedRevision,
511
+ };
512
+ const read = () => this.deliveryStore.getModelSelectionSettlement({
513
+ operationId: input.operationId, bindingId: input.bindingId, expected, payload,
514
+ });
515
+ const current = read();
516
+ if (current?.result !== undefined)
517
+ return current.result;
518
+ if (current === undefined) {
519
+ throw new AssistantDeliveryError('missing-binding', 'model selection settlement does not exist');
520
+ }
521
+ if (signal.aborted)
522
+ return undefined;
523
+ return await new Promise((resolve, reject) => {
524
+ let settled = false;
525
+ const finish = (result) => {
526
+ if (settled)
527
+ return;
528
+ settled = true;
529
+ if (waiter.pollTimer !== undefined)
530
+ clearTimeout(waiter.pollTimer);
531
+ if (waiter.deadlineTimer !== undefined)
532
+ clearTimeout(waiter.deadlineTimer);
533
+ signal.removeEventListener('abort', waiter.abort);
534
+ const waiters = this.modelSelectionWaiters.get(input.operationId);
535
+ waiters?.delete(waiter);
536
+ if (waiters?.size === 0)
537
+ this.modelSelectionWaiters.delete(input.operationId);
538
+ resolve(result);
539
+ };
540
+ const waiter = { abort: () => finish(undefined), resolve: finish };
541
+ const fail = (error) => {
542
+ if (settled)
543
+ return;
544
+ settled = true;
545
+ if (waiter.pollTimer !== undefined)
546
+ clearTimeout(waiter.pollTimer);
547
+ if (waiter.deadlineTimer !== undefined)
548
+ clearTimeout(waiter.deadlineTimer);
549
+ signal.removeEventListener('abort', waiter.abort);
550
+ const currentWaiters = this.modelSelectionWaiters.get(input.operationId);
551
+ currentWaiters?.delete(waiter);
552
+ if (currentWaiters?.size === 0)
553
+ this.modelSelectionWaiters.delete(input.operationId);
554
+ reject(error);
555
+ };
556
+ const poll = () => {
557
+ if (settled)
558
+ return;
559
+ if (signal.aborted || !this.active) {
560
+ finish(undefined);
561
+ return;
562
+ }
563
+ try {
564
+ const latest = read();
565
+ if (latest?.result !== undefined) {
566
+ finish(latest.result);
567
+ return;
568
+ }
569
+ waiter.pollTimer = setTimeout(poll, MODEL_SELECTION_WAIT_POLL_MS);
570
+ waiter.pollTimer.unref?.();
571
+ }
572
+ catch (error) {
573
+ fail(error);
574
+ }
575
+ };
576
+ const waiters = this.modelSelectionWaiters.get(input.operationId) ?? new Set();
577
+ waiters.add(waiter);
578
+ this.modelSelectionWaiters.set(input.operationId, waiters);
579
+ signal.addEventListener('abort', waiter.abort, { once: true });
580
+ waiter.deadlineTimer = setTimeout(() => finish(undefined), MODEL_SELECTION_WAIT_TIMEOUT_MS);
581
+ waiter.deadlineTimer.unref?.();
582
+ try {
583
+ const raced = read();
584
+ if (raced?.result !== undefined)
585
+ finish(raced.result);
586
+ else if (signal.aborted || !this.active)
587
+ finish(undefined);
588
+ else {
589
+ waiter.pollTimer = setTimeout(poll, MODEL_SELECTION_WAIT_POLL_MS);
590
+ waiter.pollTimer.unref?.();
591
+ }
592
+ }
593
+ catch (error) {
594
+ fail(error);
595
+ }
596
+ });
597
+ }
480
598
  advanceModelPickerForCallback(input) {
481
599
  this.assertActive();
482
600
  const binding = this.deliveryStore.getBinding(input.bindingId);
@@ -486,7 +604,12 @@ export class AssistantDeliveryService extends Service {
486
604
  || JSON.stringify(binding.principal) !== JSON.stringify(current.principal)) {
487
605
  throw new AssistantDeliveryError('missing-binding', 'model callback principal or chat does not own the active binding');
488
606
  }
489
- const picker = this.deliveryStore.getModelPicker(input.operationId, input.bindingId);
607
+ const record = this.deliveryStore.getModelPickerRecord(input.operationId, input.bindingId);
608
+ if (record === undefined || !['accepted', 'delivered', 'read'].includes(record.status)
609
+ || record.providerMessageId !== input.cardMessageId) {
610
+ throw new AssistantDeliveryError('missing-binding', 'model picker catalog or route is unavailable');
611
+ }
612
+ const picker = record.intent.modelPicker;
490
613
  if (picker === undefined || Date.now() >= picker.expiresAt
491
614
  || !pickerIncludesRoute(picker, input.expected) || !pickerIncludesRoute(picker, input.next)) {
492
615
  throw new AssistantDeliveryError('missing-binding', 'model picker catalog or route is unavailable');
@@ -507,7 +630,11 @@ export class AssistantDeliveryService extends Service {
507
630
  || JSON.stringify(binding.principal) !== JSON.stringify(current.principal)) {
508
631
  throw new AssistantDeliveryError('missing-binding', 'model callback principal or chat does not own the active binding');
509
632
  }
510
- const picker = this.deliveryStore.getModelPicker(input.operationId, input.bindingId);
633
+ const record = this.deliveryStore.getModelPickerRecord(input.operationId, input.bindingId);
634
+ if (record === undefined || !['accepted', 'delivered', 'read'].includes(record.status)
635
+ || record.providerMessageId !== input.cardMessageId)
636
+ return undefined;
637
+ const picker = record.intent.modelPicker;
511
638
  return picker === undefined || Date.now() >= picker.expiresAt ? undefined : picker;
512
639
  }
513
640
  async settlePermissionSelection(input) {
@@ -1470,8 +1597,22 @@ export class AssistantDeliveryService extends Service {
1470
1597
  clearTimeout(this.modelSelectionRetryTimer);
1471
1598
  this.timer = undefined;
1472
1599
  this.modelSelectionRetryTimer = undefined;
1600
+ this.cancelModelSelectionWaiters();
1473
1601
  await Promise.all([this.inbound.stop(), this.outbound.stop(), this.modelSelectionFlight]);
1474
1602
  }
1603
+ resolveModelSelectionWaiters(operationId, result) {
1604
+ const waiters = this.modelSelectionWaiters.get(operationId);
1605
+ if (waiters === undefined)
1606
+ return;
1607
+ for (const waiter of waiters)
1608
+ waiter.resolve(result);
1609
+ }
1610
+ cancelModelSelectionWaiters() {
1611
+ for (const waiters of this.modelSelectionWaiters.values()) {
1612
+ for (const waiter of waiters)
1613
+ waiter.resolve(undefined);
1614
+ }
1615
+ }
1475
1616
  drainApprovalDispatches() {
1476
1617
  const cursor = this.deliveryStore.getApprovalDispatchCursor();
1477
1618
  const dispatches = this.policy.listPendingApprovalDispatches(100, cursor.after);
@@ -1553,13 +1694,17 @@ export class AssistantDeliveryService extends Service {
1553
1694
  return;
1554
1695
  }
1555
1696
  await Promise.allSettled(claims.map(async (claim) => {
1556
- const finishWithoutReply = (result) => this.deliveryStore.completeModelSelectionSettlement({
1557
- operationId: claim.operationId,
1558
- payloadHash: claim.payloadHash,
1559
- result,
1560
- ownerId: this.ownerId,
1561
- fencingToken: claim.fencingToken,
1562
- });
1697
+ const finishWithoutReply = (result) => {
1698
+ const completed = this.deliveryStore.completeModelSelectionSettlement({
1699
+ operationId: claim.operationId,
1700
+ payloadHash: claim.payloadHash,
1701
+ result,
1702
+ ownerId: this.ownerId,
1703
+ fencingToken: claim.fencingToken,
1704
+ });
1705
+ this.resolveModelSelectionWaiters(claim.operationId, completed);
1706
+ return completed;
1707
+ };
1563
1708
  const payload = claim.payload;
1564
1709
  if (!isModelSelectionPayload(payload) || payload.bindingId !== claim.bindingId) {
1565
1710
  finishWithoutReply({ status: 'rejected', reason: 'authorization-revoked' });
@@ -1589,7 +1734,7 @@ export class AssistantDeliveryService extends Service {
1589
1734
  format: 'plain',
1590
1735
  });
1591
1736
  const superseded = { status: 'rejected', reason: 'selection-superseded' };
1592
- return this.deliveryStore.completeModelSelectionSettlement({
1737
+ const completed = this.deliveryStore.completeModelSelectionSettlement({
1593
1738
  operationId: claim.operationId,
1594
1739
  payloadHash: claim.payloadHash,
1595
1740
  result,
@@ -1602,6 +1747,8 @@ export class AssistantDeliveryService extends Service {
1602
1747
  ownerId: this.ownerId,
1603
1748
  fencingToken: claim.fencingToken,
1604
1749
  });
1750
+ this.resolveModelSelectionWaiters(claim.operationId, completed);
1751
+ return completed;
1605
1752
  };
1606
1753
  const initialBinding = this.deliveryStore.getBinding(claim.bindingId);
1607
1754
  if (!ownsBinding(initialBinding) || !authorized(initialBinding)) {