@dsh-enhanced/assistant-delivery 0.1.3 → 0.1.5

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
@@ -37,8 +37,6 @@ const configSchema = Schema.object({
37
37
  policyRef: Schema.string().min(1).default('owner-dm'),
38
38
  agentProvider: Schema.string().min(1).default('deepseek-official'),
39
39
  agentModel: Schema.string().min(1).default('deepseek-v4-flash'),
40
- toolCapableProviders: Schema.array(Schema.string().pattern(/^[A-Za-z0-9][A-Za-z0-9._-]{0,255}$/u)).default(['deepseek-official']),
41
- unknownRouteToolCalls: Schema.union(['allow', 'deny']).default('allow'),
42
40
  agentMaxOutputTokens: Schema.number().step(1).min(1).default(8_192),
43
41
  modelPickerTtlMs: Schema.number().step(1).min(60_000).max(86_400_000).default(900_000),
44
42
  permissionPickerTtlMs: Schema.number().step(1).min(60_000).max(86_400_000).default(900_000),
@@ -51,6 +49,8 @@ function pickerIncludesRoute(picker, route) {
51
49
  const model = picker.models.find(candidate => candidate.provider === route.provider && candidate.id === route.model);
52
50
  return model !== undefined && (route.reasoningEffort === undefined || model.effortIds.includes(route.reasoningEffort));
53
51
  }
52
+ const MODEL_SELECTION_WAIT_POLL_MS = 500;
53
+ const MODEL_SELECTION_WAIT_TIMEOUT_MS = 120_000;
54
54
  function isModelSelectionPayload(value) {
55
55
  if (value === null || typeof value !== 'object')
56
56
  return false;
@@ -58,6 +58,7 @@ function isModelSelectionPayload(value) {
58
58
  const principal = payload.principal;
59
59
  return typeof payload.callbackEventId === 'string'
60
60
  && typeof payload.callbackChatId === 'string'
61
+ && typeof payload.cardMessageId === 'string'
61
62
  && typeof payload.bindingId === 'string'
62
63
  && principal !== undefined
63
64
  && typeof principal.channel === 'string'
@@ -173,6 +174,7 @@ export class AssistantDeliveryService extends Service {
173
174
  conversationTransitions = new Map();
174
175
  agentApprovalBindings = new WeakMap();
175
176
  toolApprovalControllers = new Set();
177
+ modelSelectionWaiters = new Map();
176
178
  modelSelectionFlight;
177
179
  modelSelectionRetryTimer;
178
180
  runtime;
@@ -227,11 +229,10 @@ export class AssistantDeliveryService extends Service {
227
229
  workspace: config.defaultWorkspace, agentPreset: config.defaultAgentPreset, policyRef: config.policyRef,
228
230
  getAgentPresets: () => runtimeCtx.get('agentPresets'),
229
231
  provider: config.agentProvider, model: config.agentModel, maxOutputTokens: config.agentMaxOutputTokens,
230
- toolCapableProviders: new Set(config.toolCapableProviders),
231
- unknownRouteToolCalls: config.unknownRouteToolCalls ?? 'allow',
232
232
  modelPickerTtlMs: config.modelPickerTtlMs,
233
233
  permissionPickerTtlMs: config.permissionPickerTtlMs,
234
234
  getModelSelection: conversation => this.deliveryStore.getModelSelection(conversation),
235
+ clearStaleModelReasoningEffort: (conversation, expected) => this.deliveryStore.clearStaleModelReasoningEffort({ conversation, expected }),
235
236
  imageMaterializer,
236
237
  isInboundAuthorized: (binding, envelope) => this.isInboundAuthorized(binding, envelope),
237
238
  isPermissionController: (binding, envelope) => this.isPermissionController(binding, envelope),
@@ -277,6 +278,7 @@ export class AssistantDeliveryService extends Service {
277
278
  this.start();
278
279
  ctx.effect(() => async () => {
279
280
  this.active = false;
281
+ this.cancelModelSelectionWaiters();
280
282
  for (const controller of this.toolApprovalControllers) {
281
283
  controller.abort(new Error('assistant-delivery is stopping'));
282
284
  }
@@ -439,13 +441,18 @@ export class AssistantDeliveryService extends Service {
439
441
  || JSON.stringify(binding.principal) !== JSON.stringify(current.principal)) {
440
442
  throw new AssistantDeliveryError('missing-binding', 'model callback principal or chat does not own the active binding');
441
443
  }
442
- const picker = this.deliveryStore.getModelPicker(input.operationId, input.bindingId);
444
+ const record = this.deliveryStore.getModelPickerRecord(input.operationId, input.bindingId);
443
445
  const expected = {
444
446
  revision: input.expectedRevision,
445
447
  provider: input.modelProvider,
446
448
  model: input.model,
447
449
  ...(input.reasoningEffort === undefined ? {} : { reasoningEffort: input.reasoningEffort }),
448
450
  };
451
+ if (record === undefined || !['accepted', 'delivered', 'read'].includes(record.status)
452
+ || record.providerMessageId !== input.cardMessageId) {
453
+ throw new AssistantDeliveryError('missing-binding', 'model picker confirmation is unavailable or invalid');
454
+ }
455
+ const picker = record.intent.modelPicker;
449
456
  if (picker === undefined || !pickerIncludesRoute(picker, expected)) {
450
457
  throw new AssistantDeliveryError('missing-binding', 'model picker confirmation is unavailable or invalid');
451
458
  }
@@ -457,6 +464,7 @@ export class AssistantDeliveryService extends Service {
457
464
  const payload = {
458
465
  callbackEventId: input.callbackEventId,
459
466
  callbackChatId: input.callbackChatId,
467
+ cardMessageId: input.cardMessageId,
460
468
  bindingId: input.bindingId,
461
469
  principal: current.principal,
462
470
  provider: input.provider,
@@ -477,6 +485,112 @@ export class AssistantDeliveryService extends Service {
477
485
  void this.drainModelSelectionSettlements();
478
486
  return { status: 'pending' };
479
487
  }
488
+ async awaitModelSelection(input, signal) {
489
+ this.assertActive();
490
+ const expected = {
491
+ revision: input.expectedRevision,
492
+ provider: input.modelProvider,
493
+ model: input.model,
494
+ ...(input.reasoningEffort === undefined ? {} : { reasoningEffort: input.reasoningEffort }),
495
+ };
496
+ const payload = {
497
+ callbackEventId: input.callbackEventId,
498
+ callbackChatId: input.callbackChatId,
499
+ cardMessageId: input.cardMessageId,
500
+ bindingId: input.bindingId,
501
+ principal: input.principal,
502
+ provider: input.provider,
503
+ modelProvider: input.modelProvider,
504
+ model: input.model,
505
+ reasoningEffort: input.reasoningEffort ?? null,
506
+ expectedRevision: input.expectedRevision,
507
+ };
508
+ const read = () => this.deliveryStore.getModelSelectionSettlement({
509
+ operationId: input.operationId, bindingId: input.bindingId, expected, payload,
510
+ });
511
+ const current = read();
512
+ if (current?.result !== undefined)
513
+ return current.result;
514
+ if (current === undefined) {
515
+ throw new AssistantDeliveryError('missing-binding', 'model selection settlement does not exist');
516
+ }
517
+ if (signal.aborted)
518
+ return undefined;
519
+ return await new Promise((resolve, reject) => {
520
+ let settled = false;
521
+ const finish = (result) => {
522
+ if (settled)
523
+ return;
524
+ settled = true;
525
+ if (waiter.pollTimer !== undefined)
526
+ clearTimeout(waiter.pollTimer);
527
+ if (waiter.deadlineTimer !== undefined)
528
+ clearTimeout(waiter.deadlineTimer);
529
+ signal.removeEventListener('abort', waiter.abort);
530
+ const waiters = this.modelSelectionWaiters.get(input.operationId);
531
+ waiters?.delete(waiter);
532
+ if (waiters?.size === 0)
533
+ this.modelSelectionWaiters.delete(input.operationId);
534
+ resolve(result);
535
+ };
536
+ const waiter = { abort: () => finish(undefined), resolve: finish };
537
+ const fail = (error) => {
538
+ if (settled)
539
+ return;
540
+ settled = true;
541
+ if (waiter.pollTimer !== undefined)
542
+ clearTimeout(waiter.pollTimer);
543
+ if (waiter.deadlineTimer !== undefined)
544
+ clearTimeout(waiter.deadlineTimer);
545
+ signal.removeEventListener('abort', waiter.abort);
546
+ const currentWaiters = this.modelSelectionWaiters.get(input.operationId);
547
+ currentWaiters?.delete(waiter);
548
+ if (currentWaiters?.size === 0)
549
+ this.modelSelectionWaiters.delete(input.operationId);
550
+ reject(error);
551
+ };
552
+ const poll = () => {
553
+ if (settled)
554
+ return;
555
+ if (signal.aborted || !this.active) {
556
+ finish(undefined);
557
+ return;
558
+ }
559
+ try {
560
+ const latest = read();
561
+ if (latest?.result !== undefined) {
562
+ finish(latest.result);
563
+ return;
564
+ }
565
+ waiter.pollTimer = setTimeout(poll, MODEL_SELECTION_WAIT_POLL_MS);
566
+ waiter.pollTimer.unref?.();
567
+ }
568
+ catch (error) {
569
+ fail(error);
570
+ }
571
+ };
572
+ const waiters = this.modelSelectionWaiters.get(input.operationId) ?? new Set();
573
+ waiters.add(waiter);
574
+ this.modelSelectionWaiters.set(input.operationId, waiters);
575
+ signal.addEventListener('abort', waiter.abort, { once: true });
576
+ waiter.deadlineTimer = setTimeout(() => finish(undefined), MODEL_SELECTION_WAIT_TIMEOUT_MS);
577
+ waiter.deadlineTimer.unref?.();
578
+ try {
579
+ const raced = read();
580
+ if (raced?.result !== undefined)
581
+ finish(raced.result);
582
+ else if (signal.aborted || !this.active)
583
+ finish(undefined);
584
+ else {
585
+ waiter.pollTimer = setTimeout(poll, MODEL_SELECTION_WAIT_POLL_MS);
586
+ waiter.pollTimer.unref?.();
587
+ }
588
+ }
589
+ catch (error) {
590
+ fail(error);
591
+ }
592
+ });
593
+ }
480
594
  advanceModelPickerForCallback(input) {
481
595
  this.assertActive();
482
596
  const binding = this.deliveryStore.getBinding(input.bindingId);
@@ -486,7 +600,12 @@ export class AssistantDeliveryService extends Service {
486
600
  || JSON.stringify(binding.principal) !== JSON.stringify(current.principal)) {
487
601
  throw new AssistantDeliveryError('missing-binding', 'model callback principal or chat does not own the active binding');
488
602
  }
489
- const picker = this.deliveryStore.getModelPicker(input.operationId, input.bindingId);
603
+ const record = this.deliveryStore.getModelPickerRecord(input.operationId, input.bindingId);
604
+ if (record === undefined || !['accepted', 'delivered', 'read'].includes(record.status)
605
+ || record.providerMessageId !== input.cardMessageId) {
606
+ throw new AssistantDeliveryError('missing-binding', 'model picker catalog or route is unavailable');
607
+ }
608
+ const picker = record.intent.modelPicker;
490
609
  if (picker === undefined || Date.now() >= picker.expiresAt
491
610
  || !pickerIncludesRoute(picker, input.expected) || !pickerIncludesRoute(picker, input.next)) {
492
611
  throw new AssistantDeliveryError('missing-binding', 'model picker catalog or route is unavailable');
@@ -507,7 +626,11 @@ export class AssistantDeliveryService extends Service {
507
626
  || JSON.stringify(binding.principal) !== JSON.stringify(current.principal)) {
508
627
  throw new AssistantDeliveryError('missing-binding', 'model callback principal or chat does not own the active binding');
509
628
  }
510
- const picker = this.deliveryStore.getModelPicker(input.operationId, input.bindingId);
629
+ const record = this.deliveryStore.getModelPickerRecord(input.operationId, input.bindingId);
630
+ if (record === undefined || !['accepted', 'delivered', 'read'].includes(record.status)
631
+ || record.providerMessageId !== input.cardMessageId)
632
+ return undefined;
633
+ const picker = record.intent.modelPicker;
511
634
  return picker === undefined || Date.now() >= picker.expiresAt ? undefined : picker;
512
635
  }
513
636
  async settlePermissionSelection(input) {
@@ -1470,8 +1593,22 @@ export class AssistantDeliveryService extends Service {
1470
1593
  clearTimeout(this.modelSelectionRetryTimer);
1471
1594
  this.timer = undefined;
1472
1595
  this.modelSelectionRetryTimer = undefined;
1596
+ this.cancelModelSelectionWaiters();
1473
1597
  await Promise.all([this.inbound.stop(), this.outbound.stop(), this.modelSelectionFlight]);
1474
1598
  }
1599
+ resolveModelSelectionWaiters(operationId, result) {
1600
+ const waiters = this.modelSelectionWaiters.get(operationId);
1601
+ if (waiters === undefined)
1602
+ return;
1603
+ for (const waiter of waiters)
1604
+ waiter.resolve(result);
1605
+ }
1606
+ cancelModelSelectionWaiters() {
1607
+ for (const waiters of this.modelSelectionWaiters.values()) {
1608
+ for (const waiter of waiters)
1609
+ waiter.resolve(undefined);
1610
+ }
1611
+ }
1475
1612
  drainApprovalDispatches() {
1476
1613
  const cursor = this.deliveryStore.getApprovalDispatchCursor();
1477
1614
  const dispatches = this.policy.listPendingApprovalDispatches(100, cursor.after);
@@ -1553,13 +1690,17 @@ export class AssistantDeliveryService extends Service {
1553
1690
  return;
1554
1691
  }
1555
1692
  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
- });
1693
+ const finishWithoutReply = (result) => {
1694
+ const completed = this.deliveryStore.completeModelSelectionSettlement({
1695
+ operationId: claim.operationId,
1696
+ payloadHash: claim.payloadHash,
1697
+ result,
1698
+ ownerId: this.ownerId,
1699
+ fencingToken: claim.fencingToken,
1700
+ });
1701
+ this.resolveModelSelectionWaiters(claim.operationId, completed);
1702
+ return completed;
1703
+ };
1563
1704
  const payload = claim.payload;
1564
1705
  if (!isModelSelectionPayload(payload) || payload.bindingId !== claim.bindingId) {
1565
1706
  finishWithoutReply({ status: 'rejected', reason: 'authorization-revoked' });
@@ -1589,7 +1730,7 @@ export class AssistantDeliveryService extends Service {
1589
1730
  format: 'plain',
1590
1731
  });
1591
1732
  const superseded = { status: 'rejected', reason: 'selection-superseded' };
1592
- return this.deliveryStore.completeModelSelectionSettlement({
1733
+ const completed = this.deliveryStore.completeModelSelectionSettlement({
1593
1734
  operationId: claim.operationId,
1594
1735
  payloadHash: claim.payloadHash,
1595
1736
  result,
@@ -1602,6 +1743,8 @@ export class AssistantDeliveryService extends Service {
1602
1743
  ownerId: this.ownerId,
1603
1744
  fencingToken: claim.fencingToken,
1604
1745
  });
1746
+ this.resolveModelSelectionWaiters(claim.operationId, completed);
1747
+ return completed;
1605
1748
  };
1606
1749
  const initialBinding = this.deliveryStore.getBinding(claim.bindingId);
1607
1750
  if (!ownsBinding(initialBinding) || !authorized(initialBinding)) {