@dsh-enhanced/assistant-delivery 0.1.24 → 0.1.31

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 (66) hide show
  1. package/README.md +81 -10
  2. package/lib/acceptance.d.ts +46 -0
  3. package/lib/acceptance.d.ts.map +1 -0
  4. package/lib/acceptance.js +2 -0
  5. package/lib/acceptance.js.map +1 -0
  6. package/lib/agent-runtime.d.ts +33 -3
  7. package/lib/agent-runtime.d.ts.map +1 -1
  8. package/lib/agent-runtime.js +835 -62
  9. package/lib/agent-runtime.js.map +1 -1
  10. package/lib/coordinator.d.ts +4 -1
  11. package/lib/coordinator.d.ts.map +1 -1
  12. package/lib/coordinator.js +9 -0
  13. package/lib/coordinator.js.map +1 -1
  14. package/lib/feedback-command.d.ts +21 -1
  15. package/lib/feedback-command.d.ts.map +1 -1
  16. package/lib/feedback-command.js +18 -0
  17. package/lib/feedback-command.js.map +1 -1
  18. package/lib/goal-wake-types.d.ts +93 -0
  19. package/lib/goal-wake-types.d.ts.map +1 -0
  20. package/lib/goal-wake-types.js +70 -0
  21. package/lib/goal-wake-types.js.map +1 -0
  22. package/lib/inbound-images.d.ts.map +1 -1
  23. package/lib/inbound-images.js +11 -9
  24. package/lib/inbound-images.js.map +1 -1
  25. package/lib/index.d.ts +4 -0
  26. package/lib/index.d.ts.map +1 -1
  27. package/lib/index.js +1 -0
  28. package/lib/index.js.map +1 -1
  29. package/lib/native-web-owner.d.ts +78 -0
  30. package/lib/native-web-owner.d.ts.map +1 -0
  31. package/lib/native-web-owner.js +291 -0
  32. package/lib/native-web-owner.js.map +1 -0
  33. package/lib/operator-snapshot.d.ts +80 -0
  34. package/lib/operator-snapshot.d.ts.map +1 -0
  35. package/lib/operator-snapshot.js +935 -0
  36. package/lib/operator-snapshot.js.map +1 -0
  37. package/lib/operator.d.ts +2 -0
  38. package/lib/operator.d.ts.map +1 -1
  39. package/lib/operator.js +10 -0
  40. package/lib/operator.js.map +1 -1
  41. package/lib/service.d.ts +63 -13
  42. package/lib/service.d.ts.map +1 -1
  43. package/lib/service.js +917 -349
  44. package/lib/service.js.map +1 -1
  45. package/lib/session-lease-runtime.d.ts +69 -0
  46. package/lib/session-lease-runtime.d.ts.map +1 -0
  47. package/lib/session-lease-runtime.js +395 -0
  48. package/lib/session-lease-runtime.js.map +1 -0
  49. package/lib/session-lease-types.d.ts +27 -0
  50. package/lib/session-lease-types.d.ts.map +1 -0
  51. package/lib/session-lease-types.js +2 -0
  52. package/lib/session-lease-types.js.map +1 -0
  53. package/lib/sqlite.d.ts +1 -1
  54. package/lib/sqlite.d.ts.map +1 -1
  55. package/lib/sqlite.js +201 -3
  56. package/lib/sqlite.js.map +1 -1
  57. package/lib/store.d.ts +133 -4
  58. package/lib/store.d.ts.map +1 -1
  59. package/lib/store.js +771 -43
  60. package/lib/store.js.map +1 -1
  61. package/lib/types.d.ts +9 -2
  62. package/lib/types.d.ts.map +1 -1
  63. package/lib/types.js.map +1 -1
  64. package/lib/version.d.ts +1 -1
  65. package/lib/version.js +1 -1
  66. package/package.json +61 -44
package/lib/service.js CHANGED
@@ -3,18 +3,22 @@ import { isAbsolute } from 'node:path';
3
3
  import { createHash, randomUUID } from 'node:crypto';
4
4
  import { Context, Service } from '@deepseek-ai/cordis';
5
5
  import Schema from '@deepseek-ai/schemastery';
6
+ import { acceptanceCanonicalJson } from '@dsh-enhanced/task-acceptance-contract';
6
7
  import { approvalReviewerOf, isAutoReviewEscalation, } from '@dsh-enhanced/assistant-policy';
7
8
  import { TRUSTED_EVALUATION_PRODUCER_PROTOCOL, } from '@dsh-enhanced/assistant-evaluation';
8
9
  import { ASSISTANT_GROWTH_CONTRACT_VERSION, growthObjectDigest, validateResolvedWorkflowAutomationTemplate, validateWorkflowAutomationTemplate, validateWorkflowScope, validateWorkflowTraceProjectionReceipt, workflowArgumentShapeDigest, } from '@dsh-enhanced/assistant-growth-contract';
9
10
  import { DeliveryAdapterRegistry, DeliveryCoordinator, InboundCoordinator, } from './coordinator.js';
10
11
  import { DeliveryStore, DeliveryStoreError } from './store.js';
11
12
  import { DshDeliveryRuntime } from './agent-runtime.js';
13
+ import { DeliverySessionLeases } from './session-lease-runtime.js';
14
+ import { NativeWebOwner } from './native-web-owner.js';
15
+ import { validateOwnerGoalOutcomeFeedbackLocator, validateOwnerGoalOutcomeFeedbackProof, } from './goal-wake-types.js';
12
16
  import { InboundImageMaterializer } from './inbound-images.js';
13
17
  import { registerDeliveryTools } from './tools.js';
14
18
  import { bindingMatchesOwnerRoute, canonicalBackgroundSourceId as canonicalHostBackgroundSourceId, canonicalConversation, canonicalLocalOperatorId, canonicalOwnerRouteAuthority, externalPrincipalId, ownerRouteAuthorityHash, ownerRouteBindingSnapshot, } from './canonical.js';
15
19
  import { isExactDeliveryCommand, parseDeliveryCommand } from './session-commands.js';
16
20
  import { deliveryT1PreferenceKeys, deliveryT1PreferenceValues } from './learning-command.js';
17
- import { feedbackSignalInput, classifyNaturalPreferenceDirective, observedResponseLanguage, } from './feedback-command.js';
21
+ import { feedbackSignalInput, parseFeedbackCommand, classifyNaturalPreferenceDirective, observedResponseLanguage, } from './feedback-command.js';
18
22
  import { DELIVERY_PREFERENCE_PROJECTION_PROTOCOL, TRUSTED_DELIVERY_PRESENTATION_PRODUCER_PROTOCOL, } from './types.js';
19
23
  const trustedDeliveryPreferenceProducers = new WeakSet();
20
24
  const deliveryPreferenceProducerProbe = Symbol('assistant-delivery.preference-producer-probe');
@@ -46,6 +50,17 @@ const automationObjectiveFeedbackFooter = [
46
50
  '任务结果反馈:直接回复本消息并发送 `/feedback achieved`、`/feedback partial` 或 `/feedback not-achieved`。',
47
51
  '`helpful` 等只记录偏好,不会被当成任务成败。',
48
52
  ].join('\n');
53
+ const goalOutcomeFeedbackFooter = [
54
+ '',
55
+ '---',
56
+ '目标结果反馈:直接回复本消息并发送 `/feedback achieved`、`/feedback partial` 或 `/feedback not-achieved`。',
57
+ '`helpful` 等只记录偏好,不会被当成目标成败。',
58
+ ].join('\n');
59
+ function isGoalOutcomeFeedbackAuthority(value) {
60
+ return typeof value === 'object' && value !== null
61
+ && typeof value.issueOwnerGoalOutcomeFeedbackTarget === 'function'
62
+ && typeof value.resolveOwnerGoalOutcomeFeedbackTarget === 'function';
63
+ }
49
64
  function isAutomationDeliveryEvidenceResolver(value) {
50
65
  return typeof value === 'object' && value !== null
51
66
  && typeof value.resolveDeliveryEvidence === 'function';
@@ -246,6 +261,7 @@ const configSchema = Schema.object({
246
261
  agentModel: Schema.string().min(1).default('deepseek-v4-flash'),
247
262
  agentMaxOutputTokens: Schema.number().step(1).min(1).default(8_192),
248
263
  agentMaxAutoContinuationTurns: Schema.number().step(1).min(0).max(8).default(2),
264
+ agentGoalContinuationTimeoutMs: Schema.number().step(1).min(0).max(300_000).default(0),
249
265
  modelPickerTtlMs: Schema.number().step(1).min(60_000).max(86_400_000).default(900_000),
250
266
  permissionPickerTtlMs: Schema.number().step(1).min(60_000).max(86_400_000).default(900_000),
251
267
  toolApprovalTtlMs: Schema.number().step(1).min(1_000).max(300_000).default(300_000),
@@ -351,7 +367,7 @@ const permissionEventTypes = [
351
367
  const approvalOutcomes = ['allowed-once', 'rejected', 'cancelled', 'unavailable'];
352
368
  function currentPermissionEvents(agent) {
353
369
  const latest = new Map();
354
- for (const event of agent.session.events) {
370
+ for (const event of agent.session.snapshotEvents()) {
355
371
  const type = String(event.type);
356
372
  if (!permissionEventTypes.includes(type))
357
373
  continue;
@@ -362,70 +378,6 @@ function currentPermissionEvents(agent) {
362
378
  function signalAborted(signal) {
363
379
  return signal?.aborted === true;
364
380
  }
365
- const userQuestionTombstoneLimit = 1_024;
366
- function isUserQuestionApiProxy(value) {
367
- if (value === null || typeof value !== 'object')
368
- return false;
369
- const candidate = value;
370
- return candidate.events !== null && typeof candidate.events === 'object'
371
- && typeof candidate.events.mux === 'function'
372
- && typeof candidate.respond === 'function';
373
- }
374
- function requestedUserQuestionFrame(value) {
375
- if (value === null || typeof value !== 'object' || Array.isArray(value))
376
- return undefined;
377
- const envelope = value;
378
- if (typeof envelope.rpcId !== 'string' || envelope.rpcId.length === 0
379
- || envelope.payload === null || typeof envelope.payload !== 'object'
380
- || Array.isArray(envelope.payload))
381
- return undefined;
382
- const payload = envelope.payload;
383
- if (payload.type !== 'question/requested' || typeof payload.sessionId !== 'string'
384
- || payload.sessionId.length === 0 || !Array.isArray(payload.questions))
385
- return undefined;
386
- return {
387
- rpcId: envelope.rpcId,
388
- sessionId: payload.sessionId,
389
- questions: Object.freeze([...payload.questions]),
390
- };
391
- }
392
- function resolvedUserQuestionFrame(value) {
393
- if (value === null || typeof value !== 'object' || Array.isArray(value))
394
- return undefined;
395
- const envelope = value;
396
- if (envelope.payload === null || typeof envelope.payload !== 'object' || Array.isArray(envelope.payload)) {
397
- return undefined;
398
- }
399
- const payload = envelope.payload;
400
- if (payload.type !== 'question/resolved' || typeof payload.questionRpcId !== 'string'
401
- || payload.questionRpcId.length === 0
402
- || (payload.outcome !== 'answered' && payload.outcome !== 'cancelled'))
403
- return undefined;
404
- return { questionRpcId: payload.questionRpcId };
405
- }
406
- function userQuestionRequestHash(sessionId, questions) {
407
- try {
408
- const encoded = JSON.stringify([sessionId, questions]);
409
- return typeof encoded === 'string' ? createHash('sha256').update(encoded).digest('hex') : undefined;
410
- }
411
- catch {
412
- return undefined;
413
- }
414
- }
415
- function cancelledUserQuestionResponse(rpcId) {
416
- return {
417
- type: 'client-response',
418
- rpcId,
419
- result: {
420
- ok: false,
421
- error: {
422
- code: 'cancelled',
423
- message: 'the user cancelled ask_user_question',
424
- details: {},
425
- },
426
- },
427
- };
428
- }
429
381
  function sameUserQuestionAuthority(left, right) {
430
382
  return right !== undefined
431
383
  && right.adapter === left.adapter
@@ -438,6 +390,40 @@ function sameUserQuestionAuthority(left, right) {
438
390
  && JSON.stringify(right.binding.conversation) === JSON.stringify(left.binding.conversation)
439
391
  && JSON.stringify(right.binding.principal) === JSON.stringify(left.binding.principal);
440
392
  }
393
+ function isRecord(value) {
394
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
395
+ }
396
+ function validUserQuestionAnswer(questions, value) {
397
+ if (!isRecord(value) || Object.getPrototypeOf(value) !== Object.prototype
398
+ || Object.keys(value).some(key => key !== 'answers')
399
+ || !Array.isArray(value.answers) || value.answers.length !== questions.length)
400
+ return false;
401
+ return value.answers.every((answer, index) => {
402
+ const question = questions[index];
403
+ if (question === undefined || !isRecord(answer) || Object.getPrototypeOf(answer) !== Object.prototype
404
+ || Object.keys(answer).some(key => !['id', 'selected', 'custom'].includes(key))
405
+ || answer.id !== question.id || !Array.isArray(answer.selected)
406
+ || answer.selected.some(item => typeof item !== 'string')
407
+ || new Set(answer.selected).size !== answer.selected.length)
408
+ return false;
409
+ const optionLabels = new Set((question.options ?? []).map(option => option.label));
410
+ if (answer.selected.some(item => !optionLabels.has(item)))
411
+ return false;
412
+ const custom = answer.custom;
413
+ if (custom !== undefined && (typeof custom !== 'string' || custom.trim() === ''))
414
+ return false;
415
+ if (question.multiSelect !== true && (answer.selected.length > 1
416
+ || (answer.selected.length !== 0 && custom !== undefined)))
417
+ return false;
418
+ return true;
419
+ });
420
+ }
421
+ function userQuestionError(message, code, cause) {
422
+ return Object.assign(new Error(message, cause === undefined ? undefined : { cause }), {
423
+ name: 'UserQuestionError',
424
+ code,
425
+ });
426
+ }
441
427
  function sameToolApprovalAuthority(left, right) {
442
428
  return right !== undefined
443
429
  && right.adapter === left.adapter
@@ -516,13 +502,16 @@ export class AssistantDeliveryService extends Service {
516
502
  config;
517
503
  ownerRoutes;
518
504
  ownerRouteGuard;
505
+ nativeWebBound = false;
506
+ nativeWebOwner;
507
+ nativeWebRuntime;
519
508
  ownerId = `assistant-delivery-${randomUUID()}`;
520
509
  bindingFlights = new Map();
521
510
  conversationTransitions = new Map();
522
511
  agentApprovalBindings = new WeakMap();
523
512
  toolApprovalControllers = new Set();
524
513
  drainingUserQuestionAdapters = new WeakSet();
525
- userQuestionBridge;
514
+ userQuestionAttempts = new Set();
526
515
  modelSelectionWaiters = new Map();
527
516
  preferenceFeedbackSink;
528
517
  evaluationSink;
@@ -532,11 +521,19 @@ export class AssistantDeliveryService extends Service {
532
521
  activePresentationRegistrations = new WeakSet();
533
522
  evaluationProducerGeneration = `assistant-delivery:${randomUUID()}`;
534
523
  preferenceProducerGeneration = `assistant-delivery-preference:${randomUUID()}`;
524
+ acceptanceProducerGeneration = `assistant-delivery-acceptance:${randomUUID()}`;
525
+ acceptanceSink;
526
+ /** Once a live verifier required acceptance, loss of that verifier must not silently bypass it. */
527
+ acceptanceRequired = false;
535
528
  preferenceTurns = new WeakMap();
529
+ goalWakeResults = new WeakMap();
536
530
  modelSelectionFlight;
537
531
  presentationFlight;
538
532
  workflowTraceFlight;
539
533
  preferenceProjectionFlight;
534
+ inboundAdmissionRecoveryFlight;
535
+ inboundAdmissionRecoveryAfterSequence = 0;
536
+ inboundAdmissionRecoveryController = new AbortController();
540
537
  workflowTraceRetryTimer;
541
538
  preferenceProjectionRetryTimer;
542
539
  modelSelectionRetryTimer;
@@ -623,18 +620,36 @@ export class AssistantDeliveryService extends Service {
623
620
  ownerId: this.ownerId, leaseMs: config.leaseMs, maxAttempts: config.maxAttempts,
624
621
  maxConcurrency: config.maxConcurrency, retryBaseMs: config.retryBaseMs, retryMaxMs: config.retryMaxMs,
625
622
  ownerRouteGuard: this.ownerRouteGuard,
623
+ notificationGuard: record => this.#notificationGuard(record),
626
624
  });
627
625
  this.inbound = new InboundCoordinator({ store: this.deliveryStore, processor: () => this.runtime,
628
626
  ownerId: this.ownerId, leaseMs: config.leaseMs, maxAttempts: config.maxAttempts,
629
627
  maxConcurrency: config.maxConcurrency, retryBaseMs: config.retryBaseMs, retryMaxMs: config.retryMaxMs });
630
628
  ctx.inject(['agents', 'sessions', 'llm'], runtimeCtx => {
629
+ const sessionLeases = new DeliverySessionLeases(runtimeCtx, {
630
+ requiresLease: sessionId => this.deliveryStore.requiresSessionLease(sessionId),
631
+ leaseMs: Math.min(config.leaseMs, 300_000),
632
+ claim: (target, holderId, leaseMs) => this.deliveryStore.claimSessionLease(target, holderId, leaseMs),
633
+ dispatch: lease => this.deliveryStore.markSessionLeaseDispatched(lease),
634
+ valid: lease => this.deliveryStore.hasSessionLease(lease),
635
+ renew: (lease, leaseMs) => this.deliveryStore.renewSessionLease(lease, leaseMs),
636
+ finish: (lease, input) => this.deliveryStore.finishSessionLease(lease, input),
637
+ });
638
+ const webRuntime = { ctx: runtimeCtx, leases: sessionLeases };
639
+ this.nativeWebRuntime = webRuntime;
640
+ runtimeCtx.effect(() => () => { if (this.nativeWebRuntime === webRuntime)
641
+ this.nativeWebRuntime = undefined; });
631
642
  const unregister = this.registerInboundRuntime(new DshDeliveryRuntime(runtimeCtx, policy, {
643
+ sessionLeases,
632
644
  sessionNamespace: this.deliveryStore.instanceId(),
633
645
  workspace: config.defaultWorkspace, agentPreset: config.defaultAgentPreset, policyRef: config.policyRef,
634
646
  getAgentPresets: () => runtimeCtx.get('agentPresets'),
635
647
  provider: config.agentProvider, model: config.agentModel, maxOutputTokens: config.agentMaxOutputTokens,
636
648
  maxAutoContinuationTurns: config.agentMaxAutoContinuationTurns,
649
+ goalContinuationTimeoutMs: config.agentGoalContinuationTimeoutMs,
637
650
  maxTextBytes: config.maxTextBytes,
651
+ prepareForegroundTaskAcceptance: (binding, envelope) => this.prepareForegroundTaskAcceptance(binding, envelope),
652
+ completeForegroundTaskAcceptance: (handle, input) => this.completeForegroundTaskAcceptance(handle, input),
638
653
  modelPickerTtlMs: config.modelPickerTtlMs,
639
654
  permissionPickerTtlMs: config.permissionPickerTtlMs,
640
655
  getModelSelection: conversation => this.deliveryStore.getModelSelection(conversation),
@@ -677,15 +692,10 @@ export class AssistantDeliveryService extends Service {
677
692
  });
678
693
  ctx.inject(['approval'], approvalCtx => approvalCtx.on('approval/request', (request, next) => this.requestToolApproval(approvalCtx, request, next)));
679
694
  ctx.inject(['tools'], toolsCtx => registerDeliveryTools(toolsCtx, this));
680
- // ApiProxy owns the only `userQuestions` provider. Delivery merely bridges
681
- // its server-request mux frames to an authenticated channel adapter and
682
- // echoes a client response; it must never register a competing provider.
683
- ctx.inject(['apiProxy'], apiCtx => {
684
- const apiProxy = apiCtx.apiProxy;
685
- if (!isUserQuestionApiProxy(apiProxy))
686
- return;
687
- return this.attachUserQuestionApiProxy(apiProxy);
688
- });
695
+ // rc.1 exposes questions as an Agent-scoped waterfall. Delivery is one
696
+ // answerer in that chain, so Web and other frontends may call `next()` and
697
+ // coexist without a competing service provider or a private ApiProxy seam.
698
+ ctx.inject(['userQuestions'], questionCtx => questionCtx.on('user-questions/request', (request, next) => this.answerUserQuestion(request, next), { prepend: true }));
689
699
  // Presentation writes are intentionally reverse-bound. Delivery creates
690
700
  // the publisher closure and hands it only to the exact live domain service;
691
701
  // a public service-slot caller can no longer forge a terminal projection.
@@ -713,14 +723,19 @@ export class AssistantDeliveryService extends Service {
713
723
  this.start();
714
724
  ctx.effect(() => async () => {
715
725
  this.active = false;
726
+ // The isolated Web owner may already be unwinding concurrently. Await
727
+ // its memoized drain while the delivery Store is still available so a
728
+ // successfully disposed Agent can record its quiescent lease release.
729
+ await this.nativeWebOwner?.dispose();
716
730
  trustedDeliveryPreferenceProducers.delete(this);
717
731
  this.preferenceFeedbackSink = undefined;
732
+ this.acceptanceSink = undefined;
718
733
  this.evaluationSink = undefined;
719
734
  this.workflowTraceSink = undefined;
720
735
  this.automationPresentationBinding?.dispose();
721
736
  this.evolutionPresentationBinding?.dispose();
722
737
  this.cancelModelSelectionWaiters();
723
- this.stopUserQuestionBridge(new Error('assistant-delivery is stopping'));
738
+ this.abortUserQuestionAttempts(new Error('assistant-delivery is stopping'));
724
739
  for (const controller of this.toolApprovalControllers) {
725
740
  controller.abort(new Error('assistant-delivery is stopping'));
726
741
  }
@@ -1220,11 +1235,272 @@ export class AssistantDeliveryService extends Service {
1220
1235
  this.runtime = undefined;
1221
1236
  };
1222
1237
  }
1238
+ /** Versioned capability: idle wake Agents remain alive through exact goal settlement. */
1239
+ goalWakeSettlementVersion = () => { this.assertActive(); return 1; };
1240
+ goalWakeResultVersion = () => { this.assertActive(); return 1; };
1241
+ /** Only the live Goals service can mint this process-local wake capability. */
1242
+ async resumeScheduledGoal(input) {
1243
+ this.assertActive();
1244
+ const runtime = this.runtime;
1245
+ const denied = () => { throw new AssistantDeliveryError('policy-denied', 'scheduled goal authority is no longer current'); };
1246
+ const current = () => {
1247
+ this.assertActive();
1248
+ // Cordis creates contextual service proxies on read; proxy identity is
1249
+ // not a generation proof. The currently installed service must own the
1250
+ // exact process-local capability on every boundary.
1251
+ const producer = this.ctx.get('assistantGoals', false);
1252
+ if (this.runtime !== runtime || !(runtime instanceof DshDeliveryRuntime)
1253
+ || producer?.ownsWakeExecution?.(input) !== true)
1254
+ return denied();
1255
+ input.signal.throwIfAborted();
1256
+ if (!Number.isSafeInteger(input.deadlineAt) || Date.now() >= input.deadlineAt)
1257
+ return denied();
1258
+ const expected = input.attestation;
1259
+ const resolved = this.deliveryStore.getPreferencePrincipalForSession({ sessionId: expected.sessionId,
1260
+ workspace: expected.scope.workspace, preset: expected.scope.preset });
1261
+ if (resolved === undefined)
1262
+ return denied();
1263
+ const { binding, principal } = resolved;
1264
+ if (binding.id !== expected.bindingId || binding.version !== expected.bindingVersion
1265
+ || binding.generation !== expected.bindingGeneration || binding.sessionId !== expected.sessionId
1266
+ || externalPrincipalId(binding.principal) !== expected.principalId
1267
+ || principal.id !== expected.principalLineage.principalRecordId
1268
+ || principal.version !== expected.principalLineage.principalVersion)
1269
+ return denied();
1270
+ const decision = this.policy.authorize({ subject: { kind: 'background', id: 'assistant-goals-wake/v1',
1271
+ workspace: binding.workspace, principal: expected.principalId }, action: 'wake',
1272
+ resource: { kind: 'goal', id: 'business-context' }, context: { initiator: 'background' } });
1273
+ if (decision.effect !== 'allow')
1274
+ return denied();
1275
+ return binding;
1276
+ };
1277
+ const binding = current();
1278
+ if (typeof input.settle !== 'function')
1279
+ return denied();
1280
+ const result = await runtime.resumeScheduledGoal(binding, Object.freeze({ ...input,
1281
+ assertCurrent: (agent, phase) => {
1282
+ current();
1283
+ if (String(agent.session.id) !== binding.sessionId || agent.session.header.cwd !== binding.workspace
1284
+ || agent.session.header.agentPreset !== binding.agentPreset)
1285
+ return denied();
1286
+ input.assertCurrent(agent, phase);
1287
+ current();
1288
+ },
1289
+ beforeResume: (agent) => { current(); input.beforeResume(agent); current(); },
1290
+ settle: async (agent, signal) => { current(); signal.throwIfAborted(); await input.settle(agent, signal); signal.throwIfAborted(); current(); },
1291
+ }));
1292
+ if (result.outcome === 'succeeded') {
1293
+ current();
1294
+ if (input.includeOutput === true && result.quiescent && result.output?.trim()) {
1295
+ const text = result.output;
1296
+ this.goalWakeResults.set(input, () => {
1297
+ // Re-read the exact owner, binding generation and Session at enqueue,
1298
+ // after the caller has rechecked its Goal/source authority.
1299
+ const target = current();
1300
+ if (target.conversation.channel === 'web') {
1301
+ const owner = this.nativeWebOwner;
1302
+ // This finalizes the existing, flushed native Session reply. It does
1303
+ // not create a second send or claim that the reply was read.
1304
+ if (!this.nativeWebBound || owner === undefined)
1305
+ return denied();
1306
+ try {
1307
+ owner.assertSession(target.sessionId);
1308
+ }
1309
+ catch {
1310
+ return denied();
1311
+ }
1312
+ return Object.freeze({ kind: 'native-session', sessionId: target.sessionId });
1313
+ }
1314
+ return this.publishScheduledGoalResult(input, text, current, denied);
1315
+ });
1316
+ }
1317
+ }
1318
+ return result;
1319
+ }
1320
+ /** Finalize the captured native reply, or publish it only for a still-owned external channel. */
1321
+ enqueueScheduledGoalResult(input) {
1322
+ this.assertActive();
1323
+ const publish = this.goalWakeResults.get(input);
1324
+ if (publish === undefined)
1325
+ throw new AssistantDeliveryError('policy-denied', 'scheduled goal result is unavailable');
1326
+ return publish();
1327
+ }
1328
+ publishScheduledGoalResult(input, text, current, denied) {
1329
+ current();
1330
+ let resolved;
1331
+ try {
1332
+ resolved = input.resolveOutcomeFeedbackTarget();
1333
+ }
1334
+ catch {
1335
+ return denied();
1336
+ }
1337
+ const afterResolve = current();
1338
+ let locator;
1339
+ let proof;
1340
+ try {
1341
+ locator = validateOwnerGoalOutcomeFeedbackLocator(resolved.locator);
1342
+ proof = validateOwnerGoalOutcomeFeedbackProof(resolved.proof);
1343
+ }
1344
+ catch {
1345
+ return denied();
1346
+ }
1347
+ const goals = this.ctx.get('assistantGoals', false);
1348
+ if (!isGoalOutcomeFeedbackAuthority(goals))
1349
+ return denied();
1350
+ let capabilityProof;
1351
+ try {
1352
+ capabilityProof = validateOwnerGoalOutcomeFeedbackProof(goals.resolveOwnerGoalOutcomeFeedbackTarget(resolved.capability));
1353
+ }
1354
+ catch {
1355
+ return denied();
1356
+ }
1357
+ const finalBinding = current();
1358
+ const owner = this.deliveryStore.getPrincipal(finalBinding.principal);
1359
+ if (acceptanceCanonicalJson(locator) !== acceptanceCanonicalJson(proof.locator)
1360
+ || acceptanceCanonicalJson(proof) !== acceptanceCanonicalJson(capabilityProof)
1361
+ || finalBinding.id !== afterResolve.id || locator.bindingId !== finalBinding.id
1362
+ || locator.bindingVersion !== finalBinding.version
1363
+ || locator.bindingGeneration !== finalBinding.generation
1364
+ || locator.sessionId !== finalBinding.sessionId || locator.workspace !== finalBinding.workspace
1365
+ || locator.preset !== finalBinding.agentPreset
1366
+ || locator.principalId !== externalPrincipalId(finalBinding.principal)
1367
+ || owner?.status !== 'active' || owner.role !== 'owner'
1368
+ || locator.principalRecordId !== owner.id || locator.principalVersion !== owner.version
1369
+ || proof.goal.nativeGoalId !== input.native.goalId
1370
+ || proof.receipt.completedAt > Date.now() || proof.receipt.validUntil <= Date.now())
1371
+ return denied();
1372
+ let route;
1373
+ try {
1374
+ route = this.resolveOwnerRoute(locator.ownerRouteId);
1375
+ }
1376
+ catch {
1377
+ return denied();
1378
+ }
1379
+ if (route.binding.id !== finalBinding.id || route.binding.version !== finalBinding.version
1380
+ || route.binding.generation !== finalBinding.generation
1381
+ || route.binding.sessionId !== finalBinding.sessionId)
1382
+ return denied();
1383
+ const decision = this.policy.authorize({
1384
+ subject: { kind: 'background', id: 'assistant-goals-wake/v1',
1385
+ workspace: finalBinding.workspace, principal: locator.principalId },
1386
+ action: 'send', resource: { kind: 'message', id: finalBinding.id }, context: { initiator: 'background' },
1387
+ }, { idempotencyKey: `message-send:goal-outcome:${proof.proofDigest}:${finalBinding.id}` });
1388
+ if (decision.effect !== 'allow')
1389
+ throw policyDenied(decision);
1390
+ const rendered = `${text}${goalOutcomeFeedbackFooter}`;
1391
+ if (Buffer.byteLength(rendered, 'utf8') > this.config.maxTextBytes) {
1392
+ throw new AssistantDeliveryError('runtime-conflict', 'scheduled goal result exceeds the delivery text limit');
1393
+ }
1394
+ const situation = `goal:${locator.goalId}:definition:${proof.goal.definitionVersion}`;
1395
+ return this.deliveryStore.enqueueGoalOutcomeTarget({
1396
+ locator, proof,
1397
+ intent: {
1398
+ idempotencyKey: `goal-outcome:${proof.proofDigest}:${finalBinding.id}`,
1399
+ bindingId: finalBinding.id,
1400
+ target: { conversation: finalBinding.conversation, principal: finalBinding.principal },
1401
+ text: rendered, format: 'markdown',
1402
+ metadata: Object.freeze({
1403
+ 'dsh.learning.schemaVersion': '3', 'dsh.learning.kind': 'goal-outcome',
1404
+ 'dsh.learning.goalId': locator.goalId, 'dsh.learning.assessmentId': locator.assessmentId,
1405
+ 'dsh.learning.runId': proof.runId, 'dsh.learning.situation': situation,
1406
+ 'dsh.learning.occurredAt': String(proof.receipt.completedAt),
1407
+ 'dsh.learning.objectiveStatus': proof.receipt.objectiveStatus,
1408
+ 'dsh.learning.proofDigest': proof.proofDigest,
1409
+ }),
1410
+ },
1411
+ });
1412
+ }
1413
+ /** Private verifier producer generation; invalidated with this service. */
1414
+ trustedAcceptanceProducerGeneration = () => {
1415
+ this.assertActive();
1416
+ return this.acceptanceProducerGeneration;
1417
+ };
1418
+ registerTaskAcceptanceSink = (registration) => {
1419
+ this.assertActive();
1420
+ const verifier = this.ctx.get('assistantVerifier', false);
1421
+ if (registration.protocol !== 'assistant-verifier/host-producer/v1'
1422
+ || registration.generation !== this.acceptanceProducerGeneration
1423
+ || typeof registration.prepare !== 'function' || typeof registration.completed !== 'function'
1424
+ || typeof registration.requiresAcceptance !== 'boolean'
1425
+ || typeof verifier?.ownsTaskAcceptanceRegistration !== 'function'
1426
+ || verifier.ownsTaskAcceptanceRegistration(registration) !== true) {
1427
+ throw new AssistantDeliveryError('runtime-conflict', 'task acceptance registration is invalid');
1428
+ }
1429
+ if (this.acceptanceSink !== undefined) {
1430
+ if (this.acceptanceSink.registration === registration)
1431
+ return this.removeTaskAcceptanceSink(this.acceptanceSink.token);
1432
+ throw new AssistantDeliveryError('runtime-conflict', 'task acceptance sink is already registered');
1433
+ }
1434
+ const token = Symbol('assistant-delivery.task-acceptance');
1435
+ if (registration.requiresAcceptance)
1436
+ this.acceptanceRequired = true;
1437
+ this.acceptanceSink = Object.freeze({ token, registration });
1438
+ return this.removeTaskAcceptanceSink(token);
1439
+ };
1440
+ inspectAcceptedExecution = async (contract) => {
1441
+ this.assertActive();
1442
+ return this.deliveryStore.inspectForegroundAcceptedExecution(contract);
1443
+ };
1444
+ removeTaskAcceptanceSink(token) {
1445
+ let live = true;
1446
+ return () => {
1447
+ if (!live)
1448
+ return;
1449
+ live = false;
1450
+ if (this.acceptanceSink?.token === token)
1451
+ this.acceptanceSink = undefined;
1452
+ };
1453
+ }
1454
+ prepareForegroundTaskAcceptance(binding, envelope) {
1455
+ const sink = this.acceptanceSink;
1456
+ if (sink === undefined) {
1457
+ if (this.acceptanceRequired) {
1458
+ throw new AssistantDeliveryError('runtime-conflict', 'required task acceptance verifier is unavailable');
1459
+ }
1460
+ return undefined;
1461
+ }
1462
+ const owner = this.ownerLineageForBinding(binding);
1463
+ const inbox = this.deliveryStore.getInboxByProviderEvent(envelope.channel, envelope.account, envelope.eventId);
1464
+ const exactInbox = inbox !== undefined && inbox.status === 'claimed' && inbox.bindingId === binding.id
1465
+ && JSON.stringify(inbox.envelope) === JSON.stringify(envelope);
1466
+ if (owner === undefined || !exactInbox) {
1467
+ if (this.acceptanceRequired) {
1468
+ throw new AssistantDeliveryError('missing-binding', 'required task acceptance lacks an authenticated owner inbox');
1469
+ }
1470
+ return undefined;
1471
+ }
1472
+ const task = Object.freeze({ kind: 'foreground-turn', ref: inbox.id });
1473
+ const prepared = sink.registration.prepare(Object.freeze({
1474
+ scope: Object.freeze({ workspace: binding.workspace, preset: binding.agentPreset }),
1475
+ owner: Object.freeze({ principalRecordId: owner.principalRecordId, principalVersion: owner.principalVersion }),
1476
+ task,
1477
+ // Preserve the original inbound objective byte-for-byte.
1478
+ objective: envelope.text,
1479
+ }));
1480
+ if (prepared === null) {
1481
+ if (this.acceptanceRequired) {
1482
+ throw new AssistantDeliveryError('runtime-conflict', 'required task acceptance profile did not produce a contract');
1483
+ }
1484
+ return undefined;
1485
+ }
1486
+ this.deliveryStore.bindForegroundTaskAcceptance({ inboxId: inbox.id, contractId: prepared.contractId,
1487
+ contractDigest: prepared.contractDigest, scope: { workspace: binding.workspace, preset: binding.agentPreset },
1488
+ owner, binding, dispatchedAt: Date.now() });
1489
+ return prepared;
1490
+ }
1491
+ async completeForegroundTaskAcceptance(handle, input) {
1492
+ const sink = this.acceptanceSink;
1493
+ this.deliveryStore.finishForegroundTaskAcceptance({ contractId: handle.contractId, ...input, completedAt: Date.now() });
1494
+ if (sink === undefined && this.acceptanceRequired) {
1495
+ throw new AssistantDeliveryError('runtime-conflict', 'required task acceptance verifier is unavailable after Host completion');
1496
+ }
1497
+ if (sink !== undefined)
1498
+ await sink.registration.completed(handle);
1499
+ }
1223
1500
  registerAdapter(adapter) {
1224
1501
  this.assertActive();
1225
1502
  return this.registry.register(adapter).then(unregister => {
1226
1503
  this.drainingUserQuestionAdapters.delete(adapter);
1227
- this.dispatchPendingUserQuestions();
1228
1504
  let registered = true;
1229
1505
  return async () => {
1230
1506
  if (!registered)
@@ -1233,283 +1509,109 @@ export class AssistantDeliveryService extends Service {
1233
1509
  this.drainingUserQuestionAdapters.add(adapter);
1234
1510
  this.abortUserQuestionAttemptsForAdapter(adapter, new Error('delivery adapter is unregistering'));
1235
1511
  await unregister();
1236
- this.dispatchPendingUserQuestions();
1237
1512
  };
1238
1513
  });
1239
1514
  }
1240
- attachUserQuestionApiProxy(proxy) {
1241
- this.stopUserQuestionBridge(new Error('ApiProxy question bridge replaced'));
1242
- if (!this.active)
1243
- return () => { };
1244
- const bridge = {
1245
- proxy,
1246
- controller: new AbortController(),
1247
- pending: new Map(),
1248
- completed: new Set(),
1249
- };
1250
- this.userQuestionBridge = bridge;
1251
- void this.consumeUserQuestionMux(bridge);
1252
- let attached = true;
1253
- return () => {
1254
- if (!attached)
1255
- return;
1256
- attached = false;
1257
- this.stopUserQuestionBridge(new Error('ApiProxy question bridge disposed'), bridge);
1258
- };
1259
- }
1260
- stopUserQuestionBridge(reason, expected) {
1261
- const bridge = expected ?? this.userQuestionBridge;
1262
- if (bridge === undefined || this.userQuestionBridge !== bridge)
1263
- return;
1264
- this.userQuestionBridge = undefined;
1265
- bridge.controller.abort(reason);
1266
- for (const pending of bridge.pending.values()) {
1267
- const attempt = pending.attempt;
1268
- attempt?.detachBridgeAbort();
1269
- attempt?.controller.abort(reason);
1270
- delete pending.attempt;
1271
- }
1272
- bridge.pending.clear();
1273
- bridge.completed.clear();
1274
- }
1275
- async consumeUserQuestionMux(bridge) {
1276
- try {
1277
- const stream = bridge.proxy.events.mux({
1278
- rpcId: randomUUID(),
1279
- payload: {},
1280
- }, bridge.controller.signal);
1281
- if (stream === null || typeof stream !== 'object'
1282
- || typeof stream[Symbol.asyncIterator] !== 'function')
1283
- return;
1284
- for await (const frame of stream) {
1285
- if (!this.active || this.userQuestionBridge !== bridge || bridge.controller.signal.aborted)
1286
- return;
1287
- const resolved = resolvedUserQuestionFrame(frame);
1288
- if (resolved !== undefined) {
1289
- this.resolvePendingUserQuestion(bridge, resolved.questionRpcId);
1290
- continue;
1291
- }
1292
- const requested = requestedUserQuestionFrame(frame);
1293
- if (requested !== undefined)
1294
- this.requestUserQuestionFromMux(bridge, requested);
1295
- }
1296
- }
1297
- catch {
1298
- // ApiProxy is an in-process service. If its stream is torn down, its
1299
- // Cordis injection disposer will replace this bridge; never leak a
1300
- // rejected iterator into the Host lifecycle.
1301
- }
1302
- finally {
1303
- if (this.userQuestionBridge === bridge && !bridge.controller.signal.aborted) {
1304
- this.stopUserQuestionBridge(new Error('ApiProxy question mux ended'), bridge);
1305
- }
1306
- }
1307
- }
1308
- requestUserQuestionFromMux(bridge, frame) {
1309
- if (bridge.completed.has(frame.rpcId))
1310
- return;
1311
- const requestHash = userQuestionRequestHash(frame.sessionId, frame.questions);
1312
- if (requestHash === undefined)
1313
- return;
1314
- const existing = bridge.pending.get(frame.rpcId);
1315
- if (existing !== undefined) {
1316
- if (existing.sessionId !== frame.sessionId || existing.requestHash !== requestHash) {
1317
- this.queueUserQuestionResponse(bridge, existing, cancelledUserQuestionResponse(existing.rpcId));
1318
- }
1319
- else {
1320
- this.dispatchUserQuestion(bridge, existing);
1321
- }
1322
- return;
1515
+ abortUserQuestionAttemptsForAdapter(adapter, reason) {
1516
+ for (const attempt of this.userQuestionAttempts) {
1517
+ if (attempt.authority.adapter === adapter)
1518
+ attempt.controller.abort(reason);
1323
1519
  }
1324
- const pending = {
1325
- rpcId: frame.rpcId,
1326
- sessionId: frame.sessionId,
1327
- questions: frame.questions,
1328
- requestHash,
1329
- state: 'waiting',
1330
- responseAttempts: 0,
1331
- };
1332
- bridge.pending.set(pending.rpcId, pending);
1333
- this.dispatchUserQuestion(bridge, pending);
1334
1520
  }
1335
- resolvePendingUserQuestion(bridge, rpcId) {
1336
- this.markUserQuestionCompleted(bridge, rpcId);
1337
- const pending = bridge.pending.get(rpcId);
1338
- if (pending === undefined)
1339
- return;
1340
- bridge.pending.delete(rpcId);
1341
- const attempt = pending.attempt;
1342
- attempt?.detachBridgeAbort();
1343
- attempt?.controller.abort(new Error('ApiProxy question request resolved'));
1344
- delete pending.attempt;
1345
- }
1346
- markUserQuestionCompleted(bridge, rpcId) {
1347
- if (bridge.completed.size >= userQuestionTombstoneLimit) {
1348
- const oldest = bridge.completed.values().next().value;
1349
- if (oldest !== undefined)
1350
- bridge.completed.delete(oldest);
1351
- }
1352
- bridge.completed.add(rpcId);
1353
- }
1354
- dispatchPendingUserQuestions() {
1355
- const bridge = this.userQuestionBridge;
1356
- if (!this.active || bridge === undefined || bridge.controller.signal.aborted)
1357
- return;
1358
- for (const pending of bridge.pending.values())
1359
- this.dispatchUserQuestion(bridge, pending);
1521
+ abortUserQuestionAttempts(reason) {
1522
+ for (const attempt of this.userQuestionAttempts)
1523
+ attempt.controller.abort(reason);
1360
1524
  }
1361
- dispatchUserQuestion(bridge, pending) {
1362
- if (!this.active || this.userQuestionBridge !== bridge || bridge.controller.signal.aborted
1363
- || bridge.pending.get(pending.rpcId) !== pending || pending.state !== 'waiting')
1364
- return;
1365
- if (pending.response !== undefined) {
1366
- this.sendUserQuestionResponse(bridge, pending);
1367
- return;
1368
- }
1369
- const route = this.resolveUserQuestionRoute(pending.sessionId);
1525
+ async answerUserQuestion(request, next) {
1526
+ const agent = request.agent;
1527
+ if (!this.active || agent === undefined)
1528
+ return next();
1529
+ const route = this.resolveUserQuestionRoute(String(agent.session.id), agent);
1370
1530
  if (route.state === 'deferred')
1371
- return;
1531
+ return next();
1372
1532
  if (route.state === 'invalid') {
1373
- this.queueUserQuestionResponse(bridge, pending, cancelledUserQuestionResponse(pending.rpcId));
1374
- return;
1533
+ throw userQuestionError('delivery user-question authority is no longer valid', 'ASK_ABORTED');
1375
1534
  }
1376
1535
  const controller = new AbortController();
1377
- const abortForBridge = () => controller.abort(bridge.controller.signal.reason);
1378
- if (bridge.controller.signal.aborted)
1379
- abortForBridge();
1536
+ const abortForRequest = () => controller.abort(request.signal?.reason);
1537
+ if (request.signal?.aborted)
1538
+ abortForRequest();
1380
1539
  else
1381
- bridge.controller.signal.addEventListener('abort', abortForBridge, { once: true });
1540
+ request.signal?.addEventListener('abort', abortForRequest, { once: true });
1382
1541
  const attempt = {
1383
1542
  controller,
1384
1543
  authority: route.authority,
1385
- detachBridgeAbort: () => bridge.controller.signal.removeEventListener('abort', abortForBridge),
1544
+ detachRequestAbort: () => request.signal?.removeEventListener('abort', abortForRequest),
1386
1545
  };
1387
- pending.state = 'dispatching';
1388
- pending.attempt = attempt;
1546
+ this.userQuestionAttempts.add(attempt);
1389
1547
  const input = Object.freeze({
1390
- operationId: pending.rpcId,
1548
+ operationId: randomUUID(),
1391
1549
  bindingId: route.authority.binding.id,
1392
1550
  bindingVersion: route.authority.binding.version,
1393
1551
  bindingGeneration: route.authority.binding.generation,
1394
- sessionId: pending.sessionId,
1552
+ sessionId: String(agent.session.id),
1395
1553
  target: Object.freeze({
1396
1554
  conversation: Object.freeze({ ...route.authority.binding.conversation }),
1397
1555
  principal: Object.freeze({ ...route.authority.binding.principal }),
1398
1556
  }),
1399
- questions: pending.questions,
1557
+ questions: Object.freeze([...request.questions]),
1400
1558
  });
1401
- void Promise.resolve()
1402
- .then(() => route.authority.adapter.requestUserQuestion(input, controller.signal))
1403
- .then(outcome => this.completeUserQuestionAttempt(bridge, pending, attempt, outcome), () => this.completeUserQuestionAttempt(bridge, pending, attempt, { outcome: 'unavailable' }));
1404
- }
1405
- completeUserQuestionAttempt(bridge, pending, attempt, outcome) {
1406
- if (this.userQuestionBridge !== bridge || bridge.pending.get(pending.rpcId) !== pending
1407
- || pending.attempt !== attempt)
1408
- return;
1409
- delete pending.attempt;
1410
- attempt.detachBridgeAbort();
1411
- if (attempt.controller.signal.aborted) {
1412
- pending.state = 'waiting';
1413
- this.dispatchUserQuestion(bridge, pending);
1414
- return;
1415
- }
1416
- if (!this.active || bridge.controller.signal.aborted)
1417
- return;
1418
- const route = this.resolveUserQuestionRoute(pending.sessionId);
1419
- if (route.state === 'deferred') {
1420
- pending.state = 'waiting';
1421
- return;
1422
- }
1423
- if (route.state === 'invalid' || !sameUserQuestionAuthority(attempt.authority, route.authority)) {
1424
- this.queueUserQuestionResponse(bridge, pending, cancelledUserQuestionResponse(pending.rpcId));
1425
- return;
1426
- }
1427
- if (outcome !== null && typeof outcome === 'object'
1428
- && outcome.outcome === 'answered'
1429
- && typeof outcome.answer === 'object'
1430
- && outcome.answer !== null) {
1431
- const answer = outcome.answer;
1432
- this.queueUserQuestionResponse(bridge, pending, {
1433
- type: 'client-response',
1434
- rpcId: pending.rpcId,
1435
- result: { ok: true, value: { sessionId: pending.sessionId, answer } },
1436
- });
1437
- return;
1438
- }
1439
- this.queueUserQuestionResponse(bridge, pending, cancelledUserQuestionResponse(pending.rpcId));
1440
- }
1441
- queueUserQuestionResponse(bridge, pending, response) {
1442
- if (this.userQuestionBridge !== bridge || bridge.pending.get(pending.rpcId) !== pending)
1443
- return;
1444
- const attempt = pending.attempt;
1445
- attempt?.detachBridgeAbort();
1446
- attempt?.controller.abort(new Error('ApiProxy question response is being sent'));
1447
- delete pending.attempt;
1448
- pending.response = response;
1449
- pending.responseAttempts = 0;
1450
- pending.state = 'responding';
1451
- this.sendUserQuestionResponse(bridge, pending);
1452
- }
1453
- async sendUserQuestionResponse(bridge, pending) {
1454
- const response = pending.response;
1455
- if (response === undefined || this.userQuestionBridge !== bridge
1456
- || bridge.pending.get(pending.rpcId) !== pending || pending.state !== 'responding')
1457
- return;
1458
- let receipt;
1459
1559
  try {
1460
- receipt = await bridge.proxy.respond(response);
1461
- }
1462
- catch {
1463
- if (this.userQuestionBridge === bridge && bridge.pending.get(pending.rpcId) === pending) {
1464
- pending.responseAttempts += 1;
1465
- const retry = setTimeout(() => {
1466
- void this.sendUserQuestionResponse(bridge, pending);
1467
- }, Math.min(250 * 2 ** Math.min(pending.responseAttempts - 1, 5), 5_000));
1468
- retry.unref();
1560
+ let outcome;
1561
+ try {
1562
+ outcome = await route.authority.adapter.requestUserQuestion(input, controller.signal);
1469
1563
  }
1470
- return;
1471
- }
1472
- if (this.userQuestionBridge !== bridge || bridge.pending.get(pending.rpcId) !== pending)
1473
- return;
1474
- if (!receipt.accepted && receipt.reason === 'bad-response') {
1475
- if (response.result.ok) {
1476
- pending.response = cancelledUserQuestionResponse(pending.rpcId);
1477
- pending.responseAttempts = 0;
1478
- void this.sendUserQuestionResponse(bridge, pending);
1564
+ catch (error) {
1565
+ if (!controller.signal.aborted)
1566
+ return next();
1567
+ throw userQuestionError('ask_user_question was aborted before the user answered', 'ASK_ABORTED', controller.signal.reason ?? error);
1479
1568
  }
1480
- else {
1481
- delete pending.response;
1482
- pending.state = 'waiting';
1569
+ if (controller.signal.aborted) {
1570
+ throw userQuestionError('ask_user_question was aborted before the user answered', 'ASK_ABORTED', controller.signal.reason);
1483
1571
  }
1484
- return;
1572
+ const current = this.resolveUserQuestionRoute(String(agent.session.id), agent);
1573
+ if (current.state !== 'ready' || !sameUserQuestionAuthority(route.authority, current.authority)) {
1574
+ throw userQuestionError('delivery user-question authority changed before the answer returned', 'ASK_ABORTED');
1575
+ }
1576
+ if (isRecord(outcome) && outcome.outcome === 'answered') {
1577
+ return validUserQuestionAnswer(request.questions, outcome.answer) ? outcome.answer : next();
1578
+ }
1579
+ if (isRecord(outcome) && outcome.outcome === 'unavailable')
1580
+ return next();
1581
+ if (!isRecord(outcome) || outcome.outcome !== 'cancelled')
1582
+ return next();
1583
+ throw userQuestionError('the user cancelled ask_user_question', 'ASK_CANCELLED');
1485
1584
  }
1486
- bridge.pending.delete(pending.rpcId);
1487
- this.markUserQuestionCompleted(bridge, pending.rpcId);
1488
- }
1489
- abortUserQuestionAttemptsForAdapter(adapter, reason) {
1490
- const bridge = this.userQuestionBridge;
1491
- if (bridge === undefined)
1492
- return;
1493
- for (const pending of bridge.pending.values()) {
1494
- if (pending.attempt?.authority.adapter === adapter)
1495
- pending.attempt.controller.abort(reason);
1585
+ finally {
1586
+ this.userQuestionAttempts.delete(attempt);
1587
+ attempt.detachRequestAbort();
1496
1588
  }
1497
1589
  }
1498
- resolveUserQuestionRoute(sessionId) {
1590
+ resolveUserQuestionRoute(sessionId, agent) {
1499
1591
  try {
1500
- return this.resolveUserQuestionRouteFromStore(sessionId);
1592
+ return this.resolveUserQuestionRouteFromStore(sessionId, agent);
1501
1593
  }
1502
1594
  catch {
1503
1595
  // A transient local store failure is not proof that the owner or binding
1504
- // was revoked. Keep the ApiProxy request pending for replay/re-registration.
1596
+ // was revoked. Decline this answerer and let the waterfall continue.
1505
1597
  return { state: 'deferred' };
1506
1598
  }
1507
1599
  }
1508
- resolveUserQuestionRouteFromStore(sessionId) {
1600
+ resolveUserQuestionRouteFromStore(sessionId, agent) {
1601
+ if (agent !== undefined) {
1602
+ const agents = this.context.get('agents');
1603
+ if (agents === undefined)
1604
+ return { state: 'invalid' };
1605
+ const live = agents.get(agent.id);
1606
+ if (live !== agent || live.session !== agent.session || String(agent.session.id) !== sessionId) {
1607
+ return { state: 'invalid' };
1608
+ }
1609
+ if (!agents.roots().includes(agent))
1610
+ return { state: 'invalid' };
1611
+ }
1509
1612
  const binding = this.deliveryStore.getBindingBySession(sessionId);
1510
- // The mux carries every Web session. Absence from Delivery is not a
1511
- // revocation signal and must never let this optional bridge cancel a
1512
- // question owned only by the Web UI.
1613
+ // Absence from Delivery means this question may belong to another UI
1614
+ // answerer; decline without converting it into a cancellation.
1513
1615
  if (binding === undefined)
1514
1616
  return { state: 'deferred' };
1515
1617
  const active = this.deliveryStore.getActiveBinding(binding.conversation);
@@ -1535,6 +1637,61 @@ export class AssistantDeliveryService extends Service {
1535
1637
  ownerVersion: owner.version,
1536
1638
  } };
1537
1639
  }
1640
+ /** Trusted Host configuration only; the returned capability fixes a pre-paired Web owner and scope. */
1641
+ bindNativeWebOwner = (ctx, config) => {
1642
+ this.assertActive();
1643
+ const runtime = this.nativeWebRuntime;
1644
+ if (runtime === undefined)
1645
+ throw new AssistantDeliveryError('runtime-conflict', 'native Web runtime is unavailable');
1646
+ if (this.nativeWebBound)
1647
+ throw new AssistantDeliveryError('runtime-conflict', 'native Web owner Controller is already bound');
1648
+ let access;
1649
+ access = new NativeWebOwner(ctx, this.deliveryStore, runtime.leases, this.policy, {
1650
+ assertActive: () => {
1651
+ this.assertActive();
1652
+ if (this.nativeWebRuntime !== runtime)
1653
+ throw new AssistantDeliveryError('runtime-conflict', 'native Web runtime changed');
1654
+ },
1655
+ released: () => { if (this.nativeWebOwner === access) {
1656
+ this.nativeWebOwner = undefined;
1657
+ this.nativeWebBound = false;
1658
+ } },
1659
+ policyRef: this.config.policyRef,
1660
+ leaseMs: this.config.leaseMs,
1661
+ prepare: (binding, envelope) => this.prepareForegroundTaskAcceptance(binding, envelope),
1662
+ complete: (handle, input) => this.completeForegroundTaskAcceptance(handle, input),
1663
+ claimed: (agent, envelope, turn) => this.capturePreferenceTurn(agent, { kind: 'delivery', channel: envelope.channel, account: envelope.account, eventId: envelope.eventId }, turn),
1664
+ notifications: binding => this.deliveryStore.listOutbox({ bindingId: binding.id, limit: 50 }).filter(record => record.status === 'accepted'
1665
+ && record.intent.metadata?.['dsh.native-notice'] === 'v1' && this.#notificationGuard(record) === undefined)
1666
+ .map(record => ({ id: record.id, text: record.intent.text, createdAt: record.createdAt })),
1667
+ }, config);
1668
+ this.nativeWebOwner = access;
1669
+ this.nativeWebBound = true;
1670
+ const noticeAdapter = {
1671
+ channel: 'web', account: config.principal.account,
1672
+ capabilities: Object.freeze({ reconcileUnknownSend: true, receipts: [], formats: ['plain'] }),
1673
+ async start() { },
1674
+ send: async (intent) => {
1675
+ if (this.#nativeNoticeGuard(intent) !== undefined)
1676
+ return { outcome: 'not-sent', failureCode: 'native-notice-invalid', retryable: false };
1677
+ return { outcome: 'accepted', providerMessageId: this.#notificationProviderId(intent) };
1678
+ },
1679
+ reconcileUnknownSend: async (record) => { return record.intent.metadata?.['dsh.native-notice'] === 'v1' && this.#notificationGuard(record) === undefined ? { outcome: 'accepted', providerMessageId: this.#notificationProviderId(record.intent) } : { outcome: 'not-sent', failureCode: 'native-notice-invalid', retryable: false }; },
1680
+ };
1681
+ ctx.effect(() => {
1682
+ const registration = this.registry.register(noticeAdapter);
1683
+ void registration.catch(() => { ctx.logger.warn('assistant-delivery: native Web notification adapter registration failed'); });
1684
+ return async () => {
1685
+ try {
1686
+ const remove = await registration;
1687
+ await remove();
1688
+ }
1689
+ catch { /* Registration failure is already reported; there is no adapter to remove. */ }
1690
+ };
1691
+ }, 'assistant-delivery.native-web-notices');
1692
+ runtime.ctx.effect(() => () => access.dispose(), 'assistant-delivery.native-web-runtime');
1693
+ return access;
1694
+ };
1538
1695
  currentPreferenceTurn(agent) {
1539
1696
  this.assertActive();
1540
1697
  const attestation = this.preferenceTurns.get(agent);
@@ -1554,8 +1711,8 @@ export class AssistantDeliveryService extends Service {
1554
1711
  || lineage?.principalVersion !== attestation.principalLineage.principalVersion
1555
1712
  || !this.isOwnerFeedbackController(binding, inbox.envelope))
1556
1713
  return undefined;
1557
- const started = agent.session.events.some(event => event.type === 'turn/start' && event.data.turn === attestation.turn);
1558
- const ended = agent.session.events.some(event => event.type === 'turn/end' && event.data.turn === attestation.turn);
1714
+ const started = agent.session.snapshotEvents().some(event => event.type === 'turn/start' && event.data.turn === attestation.turn);
1715
+ const ended = agent.session.snapshotEvents().some(event => event.type === 'turn/end' && event.data.turn === attestation.turn);
1559
1716
  return started && !ended ? attestation : undefined;
1560
1717
  }
1561
1718
  preferencePrincipalForAgent(agent) {
@@ -2420,7 +2577,9 @@ export class AssistantDeliveryService extends Service {
2420
2577
  * Evolution re-proves the immutable production run; the later Inbox is a
2421
2578
  * separate evidence reference, never copied as prose.
2422
2579
  */
2423
- async dispatchObjectiveFeedback(binding, envelope, objectiveStatus) {
2580
+ async dispatchObjectiveFeedback(binding, envelope, command) {
2581
+ const parsed = typeof command === 'string' ? { kind: 'objective', objectiveStatus: command } : command;
2582
+ const objectiveStatus = parsed.kind === 'objective-status' ? 'achieved' : parsed.objectiveStatus;
2424
2583
  const replyTarget = envelope.metadata?.replyToProviderMessageId;
2425
2584
  if (typeof replyTarget !== 'string')
2426
2585
  return 'invalid-target';
@@ -2435,7 +2594,23 @@ export class AssistantDeliveryService extends Service {
2435
2594
  || JSON.stringify(target.intent.target.principal) !== JSON.stringify(binding.principal)) {
2436
2595
  return 'invalid-target';
2437
2596
  }
2597
+ const hostCommand = parseDeliveryCommand(inbox.envelope);
2598
+ if (hostCommand?.name !== 'feedback' || (inbox.envelope.attachments?.length ?? 0) !== 0
2599
+ || JSON.stringify(parseFeedbackCommand(hostCommand.rawInput)) !== JSON.stringify(parsed))
2600
+ return 'invalid-target';
2438
2601
  const metadata = target.intent.metadata;
2602
+ let goalOutcomeTarget;
2603
+ try {
2604
+ goalOutcomeTarget = this.deliveryStore.getGoalOutcomeTarget(target.id);
2605
+ }
2606
+ catch {
2607
+ return 'invalid-target';
2608
+ }
2609
+ if (goalOutcomeTarget !== undefined || metadata?.['dsh.learning.kind'] === 'goal-outcome') {
2610
+ if (goalOutcomeTarget === undefined)
2611
+ return 'invalid-target';
2612
+ return await this.dispatchGoalOutcomeFeedback(binding, inbox, target, parsed, goalOutcomeTarget);
2613
+ }
2439
2614
  // Ordinary Agent replies have no learning metadata. They are eligible
2440
2615
  // only for Delivery's local, atomic verified-workflow receipt; the Store
2441
2616
  // rebuilds the full Inbox/Outbox fence and abstains unless the source text
@@ -2449,17 +2624,82 @@ export class AssistantDeliveryService extends Service {
2449
2624
  const sourceInbox = this.deliveryStore.getInboxByProviderEvent(envelope.channel, envelope.account, sourceEventId);
2450
2625
  if (sourceInbox === undefined)
2451
2626
  return 'invalid-target';
2627
+ const lineage = this.ownerLineageForBinding(binding);
2628
+ const accepted = lineage === undefined ? null : this.deliveryStore.inspectForegroundAcceptedExecutionForOwner({
2629
+ inboxId: sourceInbox.id,
2630
+ scope: { workspace: binding.workspace, preset: binding.agentPreset },
2631
+ owner: lineage,
2632
+ bindingId: binding.id,
2633
+ });
2634
+ const canonicalForeground = accepted !== null;
2635
+ const projectForegroundOwner = async (inspectOnly) => {
2636
+ const sink = this.evaluationSink;
2637
+ if (sink === undefined || lineage === undefined)
2638
+ return undefined;
2639
+ if ((parsed.kind !== 'objective' || inspectOnly) && (!['owner-objective-revision/v1', 'owner-objective-revision/v2']
2640
+ .includes(String(sink.registration.ownerRevisionProtocol))
2641
+ || typeof sink.registration.inspect !== 'function'))
2642
+ return undefined;
2643
+ const operationId = `owner-feedback:${createHash('sha256').update(JSON.stringify([inbox.id, inbox.envelopeHash])).digest('hex')}`;
2644
+ const digest = createHash('sha256').update('assistant-delivery-foreground-objective-feedback-v1\0')
2645
+ .update(JSON.stringify([binding.workspace, binding.agentPreset, sourceInbox.id, target.id]))
2646
+ .digest('hex');
2647
+ const claims = Object.freeze({
2648
+ scope: Object.freeze({ workspace: binding.workspace, preset: binding.agentPreset }),
2649
+ situation: `foreground:${sourceInbox.id}`,
2650
+ subjectKind: 'foreground-turn', subjectRef: sourceInbox.id, runId: sourceInbox.id, outboxId: target.id,
2651
+ chatId: binding.conversation.chat, principalId: externalPrincipalId(binding.principal), bindingId: binding.id,
2652
+ objectiveStatus, ownerCommand: { ...lineage, operationId,
2653
+ action: parsed.kind === 'objective-revision' ? parsed.action : 'initial',
2654
+ ...(parsed.kind === 'objective-revision' ? { expectedVersion: parsed.expectedVersion, previousStatus: parsed.previousStatus } : {}), },
2655
+ occurredAt: inbox.receivedAt,
2656
+ initialIdempotencyKey: `assistant-delivery:foreground-objective-feedback-v1:${digest}`,
2657
+ idempotencyKey: parsed.kind === 'objective-revision' ? operationId : `assistant-delivery:foreground-objective-feedback-v1:${digest}`,
2658
+ });
2659
+ const capabilityReceipt = sink.registration.issueCapability(claims);
2660
+ if (this.evaluationSink?.token !== sink.token)
2661
+ return undefined;
2662
+ if (inspectOnly)
2663
+ return sink.registration.inspect?.(capabilityReceipt);
2664
+ const receipt = await Promise.resolve(sink.registration.append({
2665
+ capabilityReceipt, runId: sourceInbox.id, outboxId: target.id, chatId: binding.conversation.chat,
2666
+ principalId: externalPrincipalId(binding.principal), bindingId: binding.id, idempotencyKey: claims.idempotencyKey,
2667
+ }));
2668
+ return typeof receipt === 'object' && receipt !== null
2669
+ && typeof receipt.ownerFeedbackState === 'object'
2670
+ ? receipt.ownerFeedbackState
2671
+ : undefined;
2672
+ };
2673
+ if (parsed.kind === 'objective-status') {
2674
+ if (!canonicalForeground)
2675
+ return this.deliveryStore.verifiedWorkflowObjectiveState(target.id, lineage) ?? 'recorded';
2676
+ try {
2677
+ return await projectForegroundOwner(true) ?? 'unavailable';
2678
+ }
2679
+ catch {
2680
+ return 'unknown';
2681
+ }
2682
+ }
2683
+ if (canonicalForeground && this.evaluationSink === undefined)
2684
+ return 'unavailable';
2452
2685
  try {
2453
- const recorded = this.deliveryStore.commitVerifiedWorkflowTraceFeedback({
2686
+ // Evaluation commits first for accepted foreground tasks. If the sink
2687
+ // is unavailable, never acknowledge a Delivery-only success; a replay
2688
+ // can safely repeat the opaque command and then repair local workflow
2689
+ // evidence after the canonical owner revision is durable.
2690
+ const canonical = canonicalForeground ? await projectForegroundOwner(false) : undefined;
2691
+ if (canonicalForeground && canonical === undefined)
2692
+ return 'unavailable';
2693
+ const result = this.deliveryStore.commitVerifiedWorkflowTraceFeedback({
2454
2694
  binding,
2455
2695
  feedbackInboxId: inbox.id,
2456
2696
  sourceInboxId: sourceInbox.id,
2457
2697
  sourceOutboxId: target.id,
2458
2698
  objectiveStatus,
2699
+ ...(parsed.kind === 'objective-revision' ? { command: parsed } : {}),
2459
2700
  });
2460
- if (recorded.outcome === 'trace-recorded')
2461
- void this.drainWorkflowTraces();
2462
- return 'recorded';
2701
+ await this.drainWorkflowTraces();
2702
+ return canonical ?? result.ownerFeedbackState ?? 'recorded';
2463
2703
  }
2464
2704
  catch (error) {
2465
2705
  if (error instanceof DeliveryStoreError) {
@@ -2471,6 +2711,9 @@ export class AssistantDeliveryService extends Service {
2471
2711
  || error.code === 'not-found')
2472
2712
  return 'invalid-target';
2473
2713
  }
2714
+ if (typeof error === 'object' && error !== null && 'code' in error
2715
+ && ['idempotency-conflict', 'version-conflict'].includes(String(error.code)))
2716
+ return 'conflict';
2474
2717
  return 'invalid-target';
2475
2718
  }
2476
2719
  }
@@ -2525,6 +2768,10 @@ export class AssistantDeliveryService extends Service {
2525
2768
  const sink = this.evaluationSink;
2526
2769
  if (sink === undefined)
2527
2770
  return 'unavailable';
2771
+ if (parsed.kind !== 'objective' && (!['owner-objective-revision/v1', 'owner-objective-revision/v2']
2772
+ .includes(String(sink.registration.ownerRevisionProtocol))
2773
+ || typeof sink.registration.inspect !== 'function'))
2774
+ return 'unavailable';
2528
2775
  // One immutable Automation run is one learning subject. The objective
2529
2776
  // status and Inbox event deliberately do not enter the key: replays and
2530
2777
  // repeated equal judgements collapse, while an opposite judgement becomes
@@ -2532,22 +2779,32 @@ export class AssistantDeliveryService extends Service {
2532
2779
  const digest = createHash('sha256').update('assistant-delivery-objective-feedback-v2\0')
2533
2780
  .update(JSON.stringify([binding.workspace, binding.agentPreset, target.id, runId]))
2534
2781
  .digest('hex');
2782
+ const lineage = this.ownerLineageForBinding(binding);
2783
+ if (lineage === undefined)
2784
+ return 'invalid-target';
2785
+ const operationId = `owner-feedback:${createHash('sha256').update(JSON.stringify([inbox.id, inbox.envelopeHash])).digest('hex')}`;
2535
2786
  const claims = Object.freeze({
2536
2787
  scope: Object.freeze({ workspace: binding.workspace, preset: binding.agentPreset }),
2537
2788
  situation,
2538
2789
  objectiveStatus,
2790
+ ownerCommand: { ...lineage, operationId,
2791
+ action: parsed.kind === 'objective-revision' ? parsed.action : 'initial',
2792
+ ...(parsed.kind === 'objective-revision' ? { expectedVersion: parsed.expectedVersion, previousStatus: parsed.previousStatus } : {}), },
2539
2793
  runId,
2540
2794
  outboxId: target.id,
2541
2795
  chatId: binding.conversation.chat,
2542
2796
  principalId: externalPrincipalId(binding.principal),
2543
2797
  bindingId: binding.id,
2544
2798
  occurredAt,
2545
- idempotencyKey: `assistant-delivery:objective-feedback-v2:${digest}`,
2799
+ initialIdempotencyKey: `assistant-delivery:objective-feedback-v2:${digest}`,
2800
+ idempotencyKey: parsed.kind === 'objective-revision' ? operationId : `assistant-delivery:objective-feedback-v2:${digest}`,
2546
2801
  });
2547
2802
  try {
2548
2803
  const capabilityReceipt = sink.registration.issueCapability(claims);
2549
2804
  if (this.evaluationSink?.token !== sink.token)
2550
2805
  return 'unknown';
2806
+ if (parsed.kind === 'objective-status')
2807
+ return sink.registration.inspect?.(capabilityReceipt) ?? 'recorded';
2551
2808
  const receipt = await Promise.resolve(sink.registration.append({
2552
2809
  capabilityReceipt,
2553
2810
  runId,
@@ -2558,15 +2815,126 @@ export class AssistantDeliveryService extends Service {
2558
2815
  idempotencyKey: claims.idempotencyKey,
2559
2816
  }));
2560
2817
  if (typeof receipt !== 'object' || receipt === null
2561
- || receipt.idempotencyKey !== claims.idempotencyKey) {
2818
+ || typeof receipt.idempotencyKey !== 'string') {
2562
2819
  return 'unknown';
2563
2820
  }
2564
- return 'recorded';
2821
+ return receipt.ownerFeedbackState ?? 'recorded';
2822
+ }
2823
+ catch (error) {
2824
+ if (typeof error === 'object' && error !== null && 'code' in error
2825
+ && ['idempotency-conflict', 'version-conflict'].includes(String(error.code)))
2826
+ return 'conflict';
2827
+ return 'unknown';
2828
+ }
2829
+ }
2830
+ async dispatchGoalOutcomeFeedback(binding, inbox, target, parsed, durable) {
2831
+ const metadata = target.intent.metadata;
2832
+ const locator = durable.locator;
2833
+ const immutableProof = durable.proof;
2834
+ const situation = `goal:${locator.goalId}:definition:${immutableProof.goal.definitionVersion}`;
2835
+ if (metadata?.['dsh.learning.schemaVersion'] !== '3'
2836
+ || metadata['dsh.learning.kind'] !== 'goal-outcome'
2837
+ || metadata['dsh.learning.goalId'] !== locator.goalId
2838
+ || metadata['dsh.learning.assessmentId'] !== locator.assessmentId
2839
+ || metadata['dsh.learning.runId'] !== immutableProof.runId
2840
+ || metadata['dsh.learning.situation'] !== situation
2841
+ || metadata['dsh.learning.occurredAt'] !== String(immutableProof.receipt.completedAt)
2842
+ || metadata['dsh.learning.objectiveStatus'] !== immutableProof.receipt.objectiveStatus
2843
+ || metadata['dsh.learning.proofDigest'] !== immutableProof.proofDigest
2844
+ || !target.intent.text.endsWith(goalOutcomeFeedbackFooter))
2845
+ return 'invalid-target';
2846
+ const owner = this.deliveryStore.getPrincipal(binding.principal);
2847
+ if (locator.bindingId !== binding.id || locator.bindingVersion !== binding.version
2848
+ || locator.bindingGeneration !== binding.generation || locator.sessionId !== binding.sessionId
2849
+ || locator.workspace !== binding.workspace || locator.preset !== binding.agentPreset
2850
+ || locator.principalId !== externalPrincipalId(binding.principal)
2851
+ || owner?.status !== 'active' || owner.role !== 'owner'
2852
+ || locator.principalRecordId !== owner.id || locator.principalVersion !== owner.version)
2853
+ return 'invalid-target';
2854
+ let route;
2855
+ try {
2856
+ route = this.resolveOwnerRoute(locator.ownerRouteId);
2857
+ }
2858
+ catch {
2859
+ return 'invalid-target';
2860
+ }
2861
+ if (route.binding.id !== binding.id || route.binding.version !== binding.version
2862
+ || route.binding.generation !== binding.generation || route.binding.sessionId !== binding.sessionId) {
2863
+ return 'invalid-target';
2864
+ }
2865
+ const issuer = this.context.get('assistantGoals', false);
2866
+ if (!isGoalOutcomeFeedbackAuthority(issuer))
2867
+ return 'unavailable';
2868
+ let goalOutcomeCapability;
2869
+ try {
2870
+ goalOutcomeCapability = issuer.issueOwnerGoalOutcomeFeedbackTarget(locator);
2871
+ }
2872
+ catch {
2873
+ return 'invalid-target';
2874
+ }
2875
+ const resolver = this.context.get('assistantGoals', false);
2876
+ if (!isGoalOutcomeFeedbackAuthority(resolver))
2877
+ return 'unavailable';
2878
+ let currentProof;
2879
+ try {
2880
+ currentProof = validateOwnerGoalOutcomeFeedbackProof(resolver.resolveOwnerGoalOutcomeFeedbackTarget(goalOutcomeCapability));
2881
+ }
2882
+ catch {
2883
+ return 'invalid-target';
2884
+ }
2885
+ // Expiry is not an identity change: the owner is allowed to label the
2886
+ // immutable result after its verifier freshness window has elapsed.
2887
+ if (acceptanceCanonicalJson(currentProof) !== acceptanceCanonicalJson(immutableProof))
2888
+ return 'invalid-target';
2889
+ const sink = this.evaluationSink;
2890
+ const registration = sink?.registration;
2891
+ if (sink === undefined || registration?.ownerRevisionProtocol !== 'owner-objective-revision/v2'
2892
+ || typeof registration.inspect !== 'function')
2893
+ return 'unavailable';
2894
+ const objectiveStatus = parsed.kind === 'objective-status' ? 'achieved' : parsed.objectiveStatus;
2895
+ const operationId = `owner-feedback:${createHash('sha256')
2896
+ .update(JSON.stringify([inbox.id, inbox.envelopeHash])).digest('hex')}`;
2897
+ const digest = createHash('sha256').update('assistant-delivery-goal-outcome-feedback-v1\0')
2898
+ .update(JSON.stringify([binding.workspace, binding.agentPreset, locator.goalId, locator.assessmentId, target.id]))
2899
+ .digest('hex');
2900
+ const idempotencyKey = parsed.kind === 'objective-revision'
2901
+ ? operationId : `assistant-delivery:goal-outcome-feedback-v1:${digest}`;
2902
+ const claims = Object.freeze({
2903
+ scope: Object.freeze({ workspace: binding.workspace, preset: binding.agentPreset }),
2904
+ situation, subjectKind: 'goal-outcome', subjectRef: locator.assessmentId,
2905
+ runId: locator.assessmentId, outboxId: target.id, chatId: binding.conversation.chat,
2906
+ principalId: locator.principalId, bindingId: binding.id, objectiveStatus, goalOutcomeCapability,
2907
+ ownerCommand: { principalRecordId: locator.principalRecordId, principalVersion: locator.principalVersion,
2908
+ operationId, action: parsed.kind === 'objective-revision' ? parsed.action : 'initial',
2909
+ ...(parsed.kind === 'objective-revision'
2910
+ ? { expectedVersion: parsed.expectedVersion, previousStatus: parsed.previousStatus }
2911
+ : {}), },
2912
+ occurredAt: immutableProof.receipt.completedAt,
2913
+ initialIdempotencyKey: `assistant-delivery:goal-outcome-feedback-v1:${digest}`,
2914
+ idempotencyKey,
2915
+ });
2916
+ try {
2917
+ const capabilityReceipt = registration.issueCapability(claims);
2918
+ if (this.evaluationSink?.token !== sink.token)
2919
+ return 'unknown';
2920
+ if (parsed.kind === 'objective-status')
2921
+ return registration.inspect(capabilityReceipt) ?? 'recorded';
2922
+ const receipt = await Promise.resolve(registration.append({
2923
+ capabilityReceipt, runId: locator.assessmentId, outboxId: target.id,
2924
+ chatId: binding.conversation.chat, principalId: locator.principalId,
2925
+ bindingId: binding.id, idempotencyKey,
2926
+ }));
2927
+ if (typeof receipt !== 'object' || receipt === null
2928
+ || typeof receipt.idempotencyKey !== 'string')
2929
+ return 'unknown';
2930
+ return receipt
2931
+ .ownerFeedbackState ?? 'recorded';
2565
2932
  }
2566
2933
  catch (error) {
2567
2934
  if (typeof error === 'object' && error !== null && 'code' in error
2568
- && error.code === 'idempotency-conflict')
2935
+ && ['idempotency-conflict', 'version-conflict'].includes(String(error.code))) {
2569
2936
  return 'conflict';
2937
+ }
2570
2938
  return 'unknown';
2571
2939
  }
2572
2940
  }
@@ -2632,7 +3000,7 @@ export class AssistantDeliveryService extends Service {
2632
3000
  const agents = ctx.get('agents');
2633
3001
  const sessions = ctx.get('sessions');
2634
3002
  const agent = request.agent;
2635
- const events = agent.session.events;
3003
+ const events = agent.session.snapshotEvents();
2636
3004
  const reviewer = approvalReviewerOf(events);
2637
3005
  const reviewRoute = reviewer === 'user'
2638
3006
  ? 'user'
@@ -2706,7 +3074,7 @@ export class AssistantDeliveryService extends Service {
2706
3074
  ['owner', owner.id, owner.version, owner.role, owner.status, owner.linkedToId ?? null,
2707
3075
  [owner.principal.channel, owner.principal.account, owner.principal.tenant, owner.principal.user]],
2708
3076
  ['session', header.version, sessionId, header.createdAt, header.cwd ?? null,
2709
- header.parentSession ?? null, header.seedLength ?? null, header.origin ?? null,
3077
+ header.parentSession ?? null, header.isSeeded, agent.session.inheritedEventCount, header.origin ?? null,
2710
3078
  header.delegationDepth ?? null, header.agentPreset ?? null],
2711
3079
  ['route-kind', route.kind],
2712
3080
  ['review-route', reviewRoute],
@@ -2737,7 +3105,7 @@ export class AssistantDeliveryService extends Service {
2737
3105
  return next();
2738
3106
  let reviewer;
2739
3107
  try {
2740
- reviewer = approvalReviewerOf(request.agent.session.events);
3108
+ reviewer = approvalReviewerOf(request.agent.session.snapshotEvents());
2741
3109
  }
2742
3110
  catch {
2743
3111
  return 'unavailable';
@@ -2861,7 +3229,7 @@ export class AssistantDeliveryService extends Service {
2861
3229
  }
2862
3230
  return approvalOutcomes.includes(outcome) ? outcome : 'unavailable';
2863
3231
  }
2864
- async acceptInbound(envelope) {
3232
+ async acceptInbound(envelope, recoverySignal) {
2865
3233
  this.assertActive();
2866
3234
  const accepted = this.deliveryStore.acceptInbound(envelope);
2867
3235
  let record = accepted.record;
@@ -2882,7 +3250,7 @@ export class AssistantDeliveryService extends Service {
2882
3250
  const sessionCommand = parseDeliveryCommand(envelope);
2883
3251
  if (isExactDeliveryCommand(sessionCommand, 'new', 'clear')) {
2884
3252
  return await this.runConversationTransition(envelope.conversation, async () => {
2885
- await this.recoverPendingInbound(envelope, record.id);
3253
+ await this.recoverPendingInbound(envelope, record.id, recoverySignal);
2886
3254
  // A provider retry can arrive while the first copy is waiting for the
2887
3255
  // old generation to drain. Re-read after acquiring the transition:
2888
3256
  // once that copy queued the command, its rotation is already complete
@@ -2896,7 +3264,7 @@ export class AssistantDeliveryService extends Service {
2896
3264
  };
2897
3265
  }
2898
3266
  record = currentRecord;
2899
- const binding = await this.bindingForInbound(envelope, record.id);
3267
+ const binding = await this.bindingForInbound(envelope, record.id, recoverySignal);
2900
3268
  if (binding === undefined) {
2901
3269
  const failed = this.deliveryStore.getInbox(record.id);
2902
3270
  return { duplicate: accepted.duplicate, inboxId: failed.id, status: failed.status };
@@ -2911,7 +3279,7 @@ export class AssistantDeliveryService extends Service {
2911
3279
  });
2912
3280
  await this.runtime?.cancelActive?.(binding, 'new');
2913
3281
  await this.inbound.cancelUndispatchedClaims(fence.claimedInboxIds, 'new');
2914
- const rotated = await this.rotateBinding(envelope, binding, record.id);
3282
+ const rotated = await this.rotateBinding(envelope, binding, record.id, recoverySignal);
2915
3283
  record = this.deliveryStore.getInbox(record.id);
2916
3284
  if (record.status !== 'queued' || record.bindingId !== rotated.id) {
2917
3285
  throw new AssistantDeliveryError('runtime-conflict', 'new-session binding committed without its command Inbox');
@@ -2921,7 +3289,7 @@ export class AssistantDeliveryService extends Service {
2921
3289
  }
2922
3290
  if (isExactDeliveryCommand(sessionCommand, 'stop')) {
2923
3291
  return await this.runConversationTransition(envelope.conversation, async () => {
2924
- await this.recoverPendingInbound(envelope, record.id);
3292
+ await this.recoverPendingInbound(envelope, record.id, recoverySignal);
2925
3293
  const currentRecord = this.deliveryStore.getInbox(record.id);
2926
3294
  if (!['received', 'authorized'].includes(currentRecord.status)) {
2927
3295
  return {
@@ -2931,7 +3299,7 @@ export class AssistantDeliveryService extends Service {
2931
3299
  };
2932
3300
  }
2933
3301
  record = currentRecord;
2934
- const binding = await this.bindingForInbound(envelope, record.id);
3302
+ const binding = await this.bindingForInbound(envelope, record.id, recoverySignal);
2935
3303
  if (binding === undefined) {
2936
3304
  const failed = this.deliveryStore.getInbox(record.id);
2937
3305
  return { duplicate: accepted.duplicate, inboxId: failed.id, status: failed.status };
@@ -2953,7 +3321,7 @@ export class AssistantDeliveryService extends Service {
2953
3321
  // It serializes only durable admission (not Agent execution), and makes a
2954
3322
  // queued `/stop` or `/new` an unambiguous boundary for later arrivals.
2955
3323
  return await this.runConversationTransition(envelope.conversation, async () => {
2956
- await this.recoverPendingInbound(envelope, record.id);
3324
+ await this.recoverPendingInbound(envelope, record.id, recoverySignal);
2957
3325
  const currentRecord = this.deliveryStore.getInbox(record.id);
2958
3326
  if (!['received', 'authorized'].includes(currentRecord.status)) {
2959
3327
  return {
@@ -2963,7 +3331,7 @@ export class AssistantDeliveryService extends Service {
2963
3331
  };
2964
3332
  }
2965
3333
  record = currentRecord;
2966
- const binding = await this.bindingForInbound(envelope, record.id);
3334
+ const binding = await this.bindingForInbound(envelope, record.id, recoverySignal);
2967
3335
  if (binding === undefined) {
2968
3336
  const failed = this.deliveryStore.getInbox(record.id);
2969
3337
  return { duplicate: accepted.duplicate, inboxId: failed.id, status: failed.status };
@@ -2974,9 +3342,12 @@ export class AssistantDeliveryService extends Service {
2974
3342
  }
2975
3343
  enqueueBackground(input) {
2976
3344
  this.assertActive();
2977
- if (containsReservedLearningMetadata(input.metadata)) {
2978
- throw new AssistantDeliveryError('runtime-conflict', 'reserved learning metadata requires the typed Automation result seam');
3345
+ if (containsReservedLearningMetadata(input.metadata) || Object.keys(input.metadata ?? {}).some(key => key.startsWith('dsh.native-notice'))) {
3346
+ throw new AssistantDeliveryError('runtime-conflict', 'reserved metadata requires its typed delivery interface');
2979
3347
  }
3348
+ return this.enqueueBackgroundAuthorized(input);
3349
+ }
3350
+ enqueueBackgroundAuthorized(input) {
2980
3351
  const binding = this.deliveryStore.getBinding(input.bindingId);
2981
3352
  if (binding === undefined || binding.status !== 'active' || !this.hasActivePrincipal(binding)) {
2982
3353
  throw new AssistantDeliveryError('missing-binding', 'delivery binding does not exist or is revoked');
@@ -2997,6 +3368,126 @@ export class AssistantDeliveryService extends Service {
2997
3368
  format: input.format ?? 'plain',
2998
3369
  ...(input.metadata === undefined ? {} : { metadata: input.metadata }) });
2999
3370
  }
3371
+ #notificationGuard(record) {
3372
+ const metadata = record.intent.metadata;
3373
+ const nativeKeys = Object.keys(metadata ?? {}).filter(key => key.startsWith('dsh.native-notice'));
3374
+ if (nativeKeys.length > 0) {
3375
+ // Native adapter sends receive only an intent, so never bind a fabricated
3376
+ // outbox id into the goal-outcome sidecar lookup. A persisted record still
3377
+ // rejects any sidecar collision before native authority is considered.
3378
+ if (containsReservedLearningMetadata(metadata))
3379
+ return { outcome: 'not-sent', failureCode: 'native-notice-invalid', retryable: false };
3380
+ if (typeof record.id === 'string') {
3381
+ try {
3382
+ if (this.deliveryStore.getGoalOutcomeTarget(record.id) !== undefined)
3383
+ return { outcome: 'not-sent', failureCode: 'native-notice-invalid', retryable: false };
3384
+ }
3385
+ catch {
3386
+ return { outcome: 'not-sent', failureCode: 'goal-outcome-target-invalid', retryable: false };
3387
+ }
3388
+ }
3389
+ return this.#nativeNoticeGuard(record.intent);
3390
+ }
3391
+ let goalTarget;
3392
+ try {
3393
+ goalTarget = this.deliveryStore.getGoalOutcomeTarget(record.id);
3394
+ }
3395
+ catch {
3396
+ return { outcome: 'not-sent', failureCode: 'goal-outcome-target-invalid', retryable: false };
3397
+ }
3398
+ if (goalTarget !== undefined || metadata?.['dsh.learning.kind'] === 'goal-outcome') {
3399
+ if (goalTarget === undefined) {
3400
+ return { outcome: 'not-sent', failureCode: 'goal-outcome-target-missing', retryable: false };
3401
+ }
3402
+ const locator = goalTarget.locator;
3403
+ const proof = goalTarget.proof;
3404
+ const binding = this.deliveryStore.getBinding(record.intent.bindingId);
3405
+ const owner = binding === undefined ? undefined : this.deliveryStore.getPrincipal(binding.principal);
3406
+ try {
3407
+ if (proof.receipt.completedAt > Date.now() || proof.receipt.validUntil <= Date.now()) {
3408
+ throw new Error('goal outcome receipt expired');
3409
+ }
3410
+ // One captured service reference must both issue and resolve the
3411
+ // process-local capability. A replacement cannot inherit authority.
3412
+ const goals = this.context.get('assistantGoals', false);
3413
+ if (!isGoalOutcomeFeedbackAuthority(goals))
3414
+ throw new Error('Goals unavailable');
3415
+ const capability = goals.issueOwnerGoalOutcomeFeedbackTarget(locator);
3416
+ const currentGoals = this.context.get('assistantGoals', false);
3417
+ if (!isGoalOutcomeFeedbackAuthority(currentGoals))
3418
+ throw new Error('current Goals unavailable');
3419
+ const currentProof = validateOwnerGoalOutcomeFeedbackProof(currentGoals.resolveOwnerGoalOutcomeFeedbackTarget(capability));
3420
+ if (acceptanceCanonicalJson(currentProof) !== acceptanceCanonicalJson(proof)) {
3421
+ throw new Error('goal outcome proof changed');
3422
+ }
3423
+ const route = this.resolveOwnerRoute(locator.ownerRouteId);
3424
+ if (binding?.status !== 'active' || route.binding.id !== binding.id
3425
+ || route.binding.version !== locator.bindingVersion
3426
+ || route.binding.generation !== locator.bindingGeneration
3427
+ || route.binding.sessionId !== locator.sessionId || binding.workspace !== locator.workspace
3428
+ || binding.agentPreset !== locator.preset || externalPrincipalId(binding.principal) !== locator.principalId
3429
+ || owner?.status !== 'active' || owner.role !== 'owner'
3430
+ || owner.id !== locator.principalRecordId || owner.version !== locator.principalVersion) {
3431
+ throw new Error('goal outcome route changed');
3432
+ }
3433
+ const permission = this.policy.evaluate({ subject: { kind: 'background', id: 'assistant-goals-wake/v1',
3434
+ workspace: locator.workspace, principal: locator.principalId }, action: 'send',
3435
+ resource: { kind: 'message', id: binding.id }, context: { initiator: 'background' } });
3436
+ if (permission.effect !== 'allow')
3437
+ throw new Error('goal outcome send denied');
3438
+ }
3439
+ catch {
3440
+ return { outcome: 'not-sent', failureCode: 'goal-outcome-authority-revoked', retryable: false };
3441
+ }
3442
+ return undefined;
3443
+ }
3444
+ return undefined;
3445
+ }
3446
+ #nativeNoticeGuard(intent) {
3447
+ const metadata = intent.metadata;
3448
+ const nativeKeys = Object.keys(metadata ?? {}).filter(key => key.startsWith('dsh.native-notice'));
3449
+ if (nativeKeys.length === 0 || containsReservedLearningMetadata(metadata))
3450
+ return { outcome: 'not-sent', failureCode: 'native-notice-invalid', retryable: false };
3451
+ if (metadata?.['dsh.native-notice'] !== 'v1')
3452
+ return { outcome: 'not-sent', failureCode: 'native-notice-invalid', retryable: false };
3453
+ const expiresAt = Number(metadata['dsh.native-notice.expiresAt']);
3454
+ const binding = this.deliveryStore.getBinding(intent.bindingId);
3455
+ const owner = binding === undefined ? undefined : this.deliveryStore.getPrincipal(binding.principal);
3456
+ if (!Number.isSafeInteger(expiresAt) || Date.now() >= expiresAt || binding?.status !== 'active' || owner?.status !== 'active' || owner.role !== 'owner'
3457
+ || owner.id !== metadata['dsh.native-notice.ownerRecordId'] || String(owner.version) !== metadata['dsh.native-notice.ownerVersion']
3458
+ || String(binding.version) !== metadata['dsh.native-notice.bindingVersion'] || String(binding.generation) !== metadata['dsh.native-notice.bindingGeneration'] || binding.sessionId !== metadata['dsh.native-notice.sessionId'])
3459
+ return { outcome: 'not-sent', failureCode: 'native-notice-authority-expired', retryable: false };
3460
+ try {
3461
+ const sourceId = canonicalBackgroundSourceId(metadata['dsh.native-notice.sourceId']);
3462
+ const current = this.resolveOwnerRoute(metadata['dsh.native-notice.ownerRouteId']).binding;
3463
+ if (current.id !== binding.id || current.version !== binding.version || current.generation !== binding.generation || current.sessionId !== binding.sessionId)
3464
+ throw new Error('route changed');
3465
+ const permission = this.policy.evaluate({ subject: { kind: 'background', id: sourceId, workspace: binding.workspace, principal: externalPrincipalId(binding.principal) }, action: 'send', resource: { kind: 'message', id: binding.id }, context: { initiator: 'background' } });
3466
+ if (permission.effect !== 'allow')
3467
+ throw new Error('send denied');
3468
+ }
3469
+ catch {
3470
+ return { outcome: 'not-sent', failureCode: 'native-notice-policy-revoked', retryable: false };
3471
+ }
3472
+ return undefined;
3473
+ }
3474
+ #notificationProviderId(intent) { return `native-notice:${createHash('sha256').update(intent.idempotencyKey).update('\u0000').update(intent.bindingId).update('\u0000').update(intent.text).digest('hex')}`; }
3475
+ /** Queue a typed owner notice. Accepted means queued; it never asserts user read. */
3476
+ enqueueOwnerNotification(input) {
3477
+ this.assertActive();
3478
+ if (!Number.isSafeInteger(input.expiresAt) || input.expiresAt <= Date.now() || typeof input.text !== 'string' || input.text.length === 0 || input.text.length > 16_384)
3479
+ throw new AssistantDeliveryError('policy-denied', 'owner notification is invalid or expired');
3480
+ const resolved = this.resolveOwnerRoute(input.ownerRouteId);
3481
+ const binding = resolved.binding;
3482
+ const owner = this.deliveryStore.getPrincipal(binding.principal);
3483
+ if (binding.sessionId !== input.sessionId || binding.workspace !== input.scope.workspace || binding.agentPreset !== input.scope.preset
3484
+ || externalPrincipalId(binding.principal) !== input.scope.principalId || owner?.id !== input.scope.principalRecordId || owner.version !== input.scope.principalVersion)
3485
+ throw new AssistantDeliveryError('policy-denied', 'owner notification authority is no longer current');
3486
+ return this.enqueueBackgroundAuthorized({ sourceId: input.sourceId, workspace: binding.workspace, bindingId: binding.id, idempotencyKey: input.idempotencyKey, text: input.text, format: 'plain', metadata: {
3487
+ 'dsh.native-notice': 'v1', 'dsh.native-notice.sourceId': input.sourceId, 'dsh.native-notice.ownerRouteId': input.ownerRouteId, 'dsh.native-notice.expiresAt': String(input.expiresAt), 'dsh.native-notice.ownerRecordId': owner.id,
3488
+ 'dsh.native-notice.ownerVersion': String(owner.version), 'dsh.native-notice.bindingVersion': String(binding.version), 'dsh.native-notice.bindingGeneration': String(binding.generation), 'dsh.native-notice.sessionId': binding.sessionId,
3489
+ } });
3490
+ }
3000
3491
  /**
3001
3492
  * Dedicated Automation result lane. Every learning field is derived from an
3002
3493
  * authoritative Automations receipt; callers cannot supply metadata,
@@ -3402,6 +3893,7 @@ export class AssistantDeliveryService extends Service {
3402
3893
  }
3403
3894
  async tick() {
3404
3895
  this.assertActive();
3896
+ await this.drainInboundAdmissions();
3405
3897
  await this.inbound.tick();
3406
3898
  await this.drainModelSelectionSettlements();
3407
3899
  this.drainApprovalDispatches();
@@ -3419,6 +3911,7 @@ export class AssistantDeliveryService extends Service {
3419
3911
  this.presentationFlight,
3420
3912
  this.preferenceProjectionFlight,
3421
3913
  this.workflowTraceFlight,
3914
+ this.inboundAdmissionRecoveryFlight,
3422
3915
  ]);
3423
3916
  }
3424
3917
  start() {
@@ -3433,7 +3926,7 @@ export class AssistantDeliveryService extends Service {
3433
3926
  this.assertActive();
3434
3927
  await this.stopInternal();
3435
3928
  }
3436
- async ensureBinding(envelope) {
3929
+ async ensureBinding(envelope, signal) {
3437
3930
  const key = JSON.stringify(envelope.conversation);
3438
3931
  const existing = this.bindingFlights.get(key);
3439
3932
  if (existing !== undefined)
@@ -3442,11 +3935,15 @@ export class AssistantDeliveryService extends Service {
3442
3935
  if (runtime === undefined)
3443
3936
  throw new AssistantDeliveryError('runtime-unavailable', 'inbound runtime is unavailable');
3444
3937
  const promise = (async () => {
3938
+ signal?.throwIfAborted();
3445
3939
  const current = this.deliveryStore.getActiveBinding(envelope.conversation);
3446
3940
  if (current !== undefined)
3447
3941
  return current;
3448
3942
  const generation = this.deliveryStore.nextBindingGeneration(envelope.conversation);
3449
- const created = await runtime.createSession({ envelope, generation, signal: new AbortController().signal });
3943
+ const created = await runtime.createSession({
3944
+ envelope, generation, signal: signal ?? new AbortController().signal,
3945
+ });
3946
+ signal?.throwIfAborted();
3450
3947
  this.assertActive();
3451
3948
  return this.deliveryStore.createBinding({ conversation: envelope.conversation, principal: envelope.principal,
3452
3949
  workspace: created.workspace, agentPreset: created.agentPreset, sessionId: created.sessionId,
@@ -3455,14 +3952,68 @@ export class AssistantDeliveryService extends Service {
3455
3952
  this.bindingFlights.set(key, promise);
3456
3953
  return promise;
3457
3954
  }
3458
- async bindingForInbound(envelope, inboxId) {
3955
+ drainInboundAdmissions() {
3956
+ if (this.inboundAdmissionRecoveryFlight !== undefined)
3957
+ return this.inboundAdmissionRecoveryFlight;
3958
+ const flight = this.runInboundAdmissionRecovery()
3959
+ .finally(() => { if (this.inboundAdmissionRecoveryFlight === flight)
3960
+ this.inboundAdmissionRecoveryFlight = undefined; });
3961
+ this.inboundAdmissionRecoveryFlight = flight;
3962
+ return flight;
3963
+ }
3964
+ async runInboundAdmissionRecovery() {
3965
+ if (this.runtime === undefined)
3966
+ return;
3967
+ let pending = this.deliveryStore.listRecoverableInboundAdmissions({
3968
+ afterSequence: this.inboundAdmissionRecoveryAfterSequence,
3969
+ limit: 100,
3970
+ });
3971
+ if (pending.length === 0 && this.inboundAdmissionRecoveryAfterSequence !== 0) {
3972
+ this.inboundAdmissionRecoveryAfterSequence = 0;
3973
+ pending = this.deliveryStore.listRecoverableInboundAdmissions({ limit: 100 });
3974
+ }
3975
+ for (const admission of pending) {
3976
+ if (!this.active || this.runtime === undefined)
3977
+ return;
3978
+ this.inboundAdmissionRecoveryController.signal.throwIfAborted();
3979
+ this.inboundAdmissionRecoveryAfterSequence = admission.admissionCursor.sequence;
3980
+ try {
3981
+ const current = this.deliveryStore.getInbox(admission.id);
3982
+ if (current === undefined || !['received', 'authorized'].includes(current.status))
3983
+ continue;
3984
+ await this.acceptInbound(current.envelope, this.inboundAdmissionRecoveryController.signal);
3985
+ }
3986
+ catch (error) {
3987
+ if (!this.active)
3988
+ return;
3989
+ this.context.logger.warn(`assistant-delivery: deferred recovery of Inbox ${admission.id}: ${String(error)}`);
3990
+ }
3991
+ }
3992
+ }
3993
+ async bindingForInbound(envelope, inboxId, signal) {
3994
+ const admission = this.deliveryStore.getInbox(inboxId);
3995
+ if (admission === undefined || !['received', 'authorized'].includes(admission.status)) {
3996
+ return undefined;
3997
+ }
3459
3998
  let binding = this.deliveryStore.getActiveBinding(envelope.conversation);
3999
+ if (admission.status === 'authorized') {
4000
+ const recorded = admission.bindingId === undefined
4001
+ ? undefined
4002
+ : this.deliveryStore.getBinding(admission.bindingId);
4003
+ if (recorded === undefined || binding?.id !== recorded.id
4004
+ || binding.version !== recorded.version || binding.generation !== recorded.generation
4005
+ || binding.sessionId !== recorded.sessionId) {
4006
+ this.deliveryStore.deadLetterInbox(inboxId, 'inbound-binding-superseded');
4007
+ return undefined;
4008
+ }
4009
+ binding = recorded;
4010
+ }
3460
4011
  if (binding === undefined) {
3461
4012
  if (this.runtime === undefined) {
3462
4013
  this.deliveryStore.deadLetterInbox(inboxId, 'runtime-unavailable');
3463
4014
  return undefined;
3464
4015
  }
3465
- binding = await this.ensureBinding(envelope);
4016
+ binding = await this.ensureBinding(envelope, signal);
3466
4017
  }
3467
4018
  if (externalPrincipalId(binding.principal) !== externalPrincipalId(envelope.principal)) {
3468
4019
  this.deliveryStore.deadLetterInbox(inboxId, 'binding-principal-mismatch');
@@ -3477,8 +4028,9 @@ export class AssistantDeliveryService extends Service {
3477
4028
  * original durable Inbox order. This includes ordinary messages that were
3478
4029
  * already persisted behind a blocked `/stop` or `/new` transition.
3479
4030
  */
3480
- async recoverPendingInbound(boundary, beforeInboxId) {
4031
+ async recoverPendingInbound(boundary, beforeInboxId, signal) {
3481
4032
  while (true) {
4033
+ signal?.throwIfAborted();
3482
4034
  const pending = this.deliveryStore.findPendingInboundBefore({
3483
4035
  conversation: boundary.conversation,
3484
4036
  principal: boundary.principal,
@@ -3503,7 +4055,8 @@ export class AssistantDeliveryService extends Service {
3503
4055
  const command = parseDeliveryCommand(pending.envelope);
3504
4056
  const reset = isExactDeliveryCommand(command, 'new', 'clear');
3505
4057
  const stop = isExactDeliveryCommand(command, 'stop');
3506
- const binding = await this.bindingForInbound(pending.envelope, pending.id);
4058
+ const binding = await this.bindingForInbound(pending.envelope, pending.id, signal);
4059
+ signal?.throwIfAborted();
3507
4060
  if (binding === undefined)
3508
4061
  continue;
3509
4062
  if (!reset && !stop) {
@@ -3517,9 +4070,11 @@ export class AssistantDeliveryService extends Service {
3517
4070
  });
3518
4071
  const cancellationCommand = reset ? 'new' : 'stop';
3519
4072
  await this.runtime?.cancelActive?.(binding, cancellationCommand);
4073
+ signal?.throwIfAborted();
3520
4074
  await this.inbound.cancelUndispatchedClaims(fence.claimedInboxIds, cancellationCommand);
4075
+ signal?.throwIfAborted();
3521
4076
  if (reset) {
3522
- await this.rotateBinding(pending.envelope, binding, pending.id);
4077
+ await this.rotateBinding(pending.envelope, binding, pending.id, signal);
3523
4078
  }
3524
4079
  else {
3525
4080
  this.deliveryStore.queueInbox(pending.id, binding.id);
@@ -3543,7 +4098,7 @@ export class AssistantDeliveryService extends Service {
3543
4098
  this.conversationTransitions.delete(key);
3544
4099
  }
3545
4100
  }
3546
- async rotateBinding(envelope, previous, commandInboxId) {
4101
+ async rotateBinding(envelope, previous, commandInboxId, signal) {
3547
4102
  const key = `${JSON.stringify(envelope.conversation)}:/new:${envelope.eventId}`;
3548
4103
  const existing = this.bindingFlights.get(key);
3549
4104
  if (existing !== undefined)
@@ -3552,6 +4107,7 @@ export class AssistantDeliveryService extends Service {
3552
4107
  if (runtime === undefined)
3553
4108
  throw new AssistantDeliveryError('runtime-unavailable', 'inbound runtime is unavailable');
3554
4109
  const promise = (async () => {
4110
+ signal?.throwIfAborted();
3555
4111
  const current = this.deliveryStore.getActiveBinding(envelope.conversation);
3556
4112
  if (current === undefined)
3557
4113
  throw new AssistantDeliveryError('missing-binding', 'active binding disappeared during /new');
@@ -3562,7 +4118,8 @@ export class AssistantDeliveryService extends Service {
3562
4118
  throw new AssistantDeliveryError('runtime-conflict', 'binding generation changed before /new session creation');
3563
4119
  }
3564
4120
  const created = await runtime.createSession({ envelope, generation,
3565
- previous: current, signal: new AbortController().signal });
4121
+ previous: current, signal: signal ?? new AbortController().signal });
4122
+ signal?.throwIfAborted();
3566
4123
  this.assertActive();
3567
4124
  return this.deliveryStore.rotateBindingAndQueueCommand({
3568
4125
  bindingId: current.id,
@@ -3588,9 +4145,20 @@ export class AssistantDeliveryService extends Service {
3588
4145
  this.workflowTraceRetryTimer = undefined;
3589
4146
  this.preferenceProjectionRetryTimer = undefined;
3590
4147
  this.cancelModelSelectionWaiters();
4148
+ this.inboundAdmissionRecoveryController.abort(new Error('assistant-delivery is stopping'));
4149
+ const admissionRecovery = this.inboundAdmissionRecoveryFlight === undefined
4150
+ ? undefined
4151
+ : Promise.race([
4152
+ this.inboundAdmissionRecoveryFlight,
4153
+ new Promise(resolve => {
4154
+ const timeout = setTimeout(resolve, Math.max(1, Math.floor(this.config.leaseMs / 3)));
4155
+ timeout.unref?.();
4156
+ }),
4157
+ ]);
3591
4158
  await Promise.all([
3592
4159
  this.inbound.stop(),
3593
4160
  this.outbound.stop(),
4161
+ admissionRecovery,
3594
4162
  this.modelSelectionFlight,
3595
4163
  this.presentationFlight,
3596
4164
  this.preferenceProjectionFlight,