@dsh-enhanced/assistant-delivery 0.1.65 → 0.1.67

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 (101) hide show
  1. package/README.md +128 -4
  2. package/lib/agent-runtime.d.ts +6 -2
  3. package/lib/agent-runtime.d.ts.map +1 -1
  4. package/lib/agent-runtime.js +60 -14
  5. package/lib/agent-runtime.js.map +1 -1
  6. package/lib/attention-audit-store.d.ts +54 -0
  7. package/lib/attention-audit-store.d.ts.map +1 -0
  8. package/lib/attention-audit-store.js +213 -0
  9. package/lib/attention-audit-store.js.map +1 -0
  10. package/lib/attention-enforcement.d.ts +75 -0
  11. package/lib/attention-enforcement.d.ts.map +1 -0
  12. package/lib/attention-enforcement.js +84 -0
  13. package/lib/attention-enforcement.js.map +1 -0
  14. package/lib/attention.d.ts +203 -0
  15. package/lib/attention.d.ts.map +1 -0
  16. package/lib/attention.js +288 -0
  17. package/lib/attention.js.map +1 -0
  18. package/lib/coordinator.d.ts.map +1 -1
  19. package/lib/coordinator.js +6 -0
  20. package/lib/coordinator.js.map +1 -1
  21. package/lib/feedback-command.d.ts +23 -0
  22. package/lib/feedback-command.d.ts.map +1 -1
  23. package/lib/feedback-command.js +176 -0
  24. package/lib/feedback-command.js.map +1 -1
  25. package/lib/foreground-observation.d.ts +21 -0
  26. package/lib/foreground-observation.d.ts.map +1 -1
  27. package/lib/foreground-observation.js.map +1 -1
  28. package/lib/foreground-termination.d.ts +31 -0
  29. package/lib/foreground-termination.d.ts.map +1 -0
  30. package/lib/foreground-termination.js +42 -0
  31. package/lib/foreground-termination.js.map +1 -0
  32. package/lib/in-process-reload-runtime.d.ts +87 -0
  33. package/lib/in-process-reload-runtime.d.ts.map +1 -0
  34. package/lib/in-process-reload-runtime.js +319 -0
  35. package/lib/in-process-reload-runtime.js.map +1 -0
  36. package/lib/index.d.ts +20 -6
  37. package/lib/index.d.ts.map +1 -1
  38. package/lib/index.js +9 -3
  39. package/lib/index.js.map +1 -1
  40. package/lib/native-termination.d.ts +12 -0
  41. package/lib/native-termination.d.ts.map +1 -0
  42. package/lib/native-termination.js +57 -0
  43. package/lib/native-termination.js.map +1 -0
  44. package/lib/native-web-owner.d.ts +7 -0
  45. package/lib/native-web-owner.d.ts.map +1 -1
  46. package/lib/native-web-owner.js +40 -10
  47. package/lib/native-web-owner.js.map +1 -1
  48. package/lib/operator-snapshot.d.ts +31 -3
  49. package/lib/operator-snapshot.d.ts.map +1 -1
  50. package/lib/operator-snapshot.js +120 -47
  51. package/lib/operator-snapshot.js.map +1 -1
  52. package/lib/progress-attention.d.ts +55 -0
  53. package/lib/progress-attention.d.ts.map +1 -0
  54. package/lib/progress-attention.js +128 -0
  55. package/lib/progress-attention.js.map +1 -0
  56. package/lib/service.d.ts +515 -4
  57. package/lib/service.d.ts.map +1 -1
  58. package/lib/service.js +2183 -130
  59. package/lib/service.js.map +1 -1
  60. package/lib/sqlite.d.ts +1 -1
  61. package/lib/sqlite.d.ts.map +1 -1
  62. package/lib/sqlite.js +102 -2
  63. package/lib/sqlite.js.map +1 -1
  64. package/lib/store.d.ts +113 -2
  65. package/lib/store.d.ts.map +1 -1
  66. package/lib/store.js +450 -39
  67. package/lib/store.js.map +1 -1
  68. package/lib/types.d.ts +175 -0
  69. package/lib/types.d.ts.map +1 -1
  70. package/lib/types.js.map +1 -1
  71. package/lib/version.d.ts +1 -1
  72. package/lib/version.js +1 -1
  73. package/lib/work-item-engineering-context.d.ts +127 -0
  74. package/lib/work-item-engineering-context.d.ts.map +1 -0
  75. package/lib/work-item-engineering-context.js +151 -0
  76. package/lib/work-item-engineering-context.js.map +1 -0
  77. package/lib/work-item-feedback-schema.d.ts +2 -0
  78. package/lib/work-item-feedback-schema.d.ts.map +1 -0
  79. package/lib/work-item-feedback-schema.js +29 -0
  80. package/lib/work-item-feedback-schema.js.map +1 -0
  81. package/lib/work-item-feedback-store.d.ts +29 -0
  82. package/lib/work-item-feedback-store.d.ts.map +1 -0
  83. package/lib/work-item-feedback-store.js +111 -0
  84. package/lib/work-item-feedback-store.js.map +1 -0
  85. package/lib/work-item-feedback.d.ts +157 -0
  86. package/lib/work-item-feedback.d.ts.map +1 -0
  87. package/lib/work-item-feedback.js +2 -0
  88. package/lib/work-item-feedback.js.map +1 -0
  89. package/lib/work-item-replies-schema.d.ts +3 -0
  90. package/lib/work-item-replies-schema.d.ts.map +1 -0
  91. package/lib/work-item-replies-schema.js +45 -0
  92. package/lib/work-item-replies-schema.js.map +1 -0
  93. package/lib/work-item-replies-store.d.ts +43 -0
  94. package/lib/work-item-replies-store.d.ts.map +1 -0
  95. package/lib/work-item-replies-store.js +203 -0
  96. package/lib/work-item-replies-store.js.map +1 -0
  97. package/lib/work-item-replies.d.ts +93 -0
  98. package/lib/work-item-replies.d.ts.map +1 -0
  99. package/lib/work-item-replies.js +15 -0
  100. package/lib/work-item-replies.js.map +1 -0
  101. package/package.json +24 -24
package/lib/service.js CHANGED
@@ -1,7 +1,10 @@
1
+ import { engineeringContextData, validateWorkItemEngineeringMaterial, frameWorkItemEngineeringContext, pageWorkItemEngineeringContext } from './work-item-engineering-context.js';
2
+ import { workItemReplyCanonical, workItemReplyDigest } from './work-item-replies.js';
1
3
  import { chmodSync, mkdirSync, readFileSync } from 'node:fs';
2
4
  import { isAbsolute } from 'node:path';
3
- import { createHash, randomUUID } from 'node:crypto';
4
- import { Context, Service } from '@deepseek-ai/cordis';
5
+ import { isProxy } from 'node:util/types';
6
+ import { createHash, randomBytes, randomUUID } from 'node:crypto';
7
+ import { Context, Service, symbols } from '@deepseek-ai/cordis';
5
8
  import Schema from '@deepseek-ai/schemastery';
6
9
  import { RestartStore } from './restart-store.js';
7
10
  import { restartDigest } from './restart-runtime.js';
@@ -22,12 +25,15 @@ import { NativeWebOwner } from './native-web-owner.js';
22
25
  import { validateOwnerGoalOutcomeFeedbackLocator, validateOwnerGoalOutcomeFeedbackProof, } from './goal-wake-types.js';
23
26
  import { isSynchronousForegroundObservationHandle, validForegroundObservationRegistration } from './foreground-observation.js';
24
27
  import { InboundImageMaterializer } from './inbound-images.js';
28
+ import { assessAttention, assessAttentionSplit, attentionOverCap, attentionSplitWindowMs, convertOverCapAttention, NOTIFICATION_OUTCOME_FACTS_PROTOCOL, linkedReportKeyPrefix, linkedReportMetadataPrefix } from './attention.js';
29
+ import { ProgressAttentionAudit } from './progress-attention.js';
30
+ import { AttentionRejectionLog, attentionEnforcementReasons } from './attention-enforcement.js';
25
31
  import { registerDeliveryTools } from './tools.js';
26
32
  import { bindingMatchesOwnerRoute, canonicalBackgroundSourceId as canonicalHostBackgroundSourceId, canonicalConversation, canonicalLocalOperatorId, canonicalOwnerRouteAuthority, externalPrincipalId, ownerRouteAuthorityHash, ownerRouteBindingSnapshot, } from './canonical.js';
27
33
  import { isExactDeliveryCommand, parseDeliveryCommand } from './session-commands.js';
28
34
  import { deliveryT1PreferenceKeys, deliveryT1PreferenceValues } from './learning-command.js';
29
35
  import { assertReplyReplayAllowed, blockAgentRepliesForReplay as createReplyReplayBlock, } from './reply-replay.js';
30
- import { feedbackSignalInput, parseFeedbackCommand, parseNaturalObjectiveFeedback, classifyNaturalPreferenceDirective, observedResponseLanguage, } from './feedback-command.js';
36
+ import { feedbackSignalInput, parseFeedbackCommand, parseNaturalObjectiveFeedback, parseNaturalOwnerReply, objectiveStateMessage, classifyNaturalPreferenceDirective, observedResponseLanguage, } from './feedback-command.js';
31
37
  import { DELIVERY_PREFERENCE_PROJECTION_PROTOCOL, TRUSTED_DELIVERY_PRESENTATION_PRODUCER_PROTOCOL, } from './types.js';
32
38
  const trustedDeliveryPreferenceProducers = new WeakSet();
33
39
  const deliveryPreferenceProducerProbe = Symbol('assistant-delivery.preference-producer-probe');
@@ -242,9 +248,24 @@ function validateLearningControlReceipt(value, request) {
242
248
  throw new Error('learning rollback receipt is invalid');
243
249
  }
244
250
  }
251
+ function workItemPeerIdentity(value) {
252
+ if (value === null || (typeof value !== 'object' && typeof value !== 'function'))
253
+ return value;
254
+ try {
255
+ return Reflect.get(value, symbols.original) ?? value;
256
+ }
257
+ catch {
258
+ return value;
259
+ }
260
+ }
261
+ /** Cordis service slot names that may own an owner-notice withdrawal check. */
262
+ const ownerNotificationAuthorityPattern = /^[A-Za-z][A-Za-z0-9]{0,63}$/u;
263
+ function ownerNotificationWithdrawalKey(authority, sourceId) { return `${authority}\u0000${sourceId}`; }
245
264
  function containsReservedLearningMetadata(metadata) {
246
265
  return metadata !== undefined && Object.keys(metadata).some(key => key.startsWith(learningMetadataPrefix));
247
266
  }
267
+ export const ownerNotificationWithdrawalProtocol = 'assistant-delivery/owner-notification-withdrawal/v1';
268
+ export const ownerNotificationReplyProtocol = 'assistant-delivery/owner-notification-reply/v1';
248
269
  export class AssistantDeliveryError extends Error {
249
270
  code;
250
271
  constructor(code, message) {
@@ -253,6 +274,17 @@ export class AssistantDeliveryError extends Error {
253
274
  this.name = 'AssistantDeliveryError';
254
275
  }
255
276
  }
277
+ /** WP19 stage 3 rejection: `policy-denied` with structured reason codes; nothing was queued. */
278
+ export class AttentionRejectedError extends AssistantDeliveryError {
279
+ rejection;
280
+ reasons;
281
+ constructor(rejection, message = `attention rejected: ${rejection.reasons.join(', ')}`) {
282
+ super('policy-denied', message);
283
+ this.rejection = rejection;
284
+ this.name = 'AttentionRejectedError';
285
+ this.reasons = rejection.reasons;
286
+ }
287
+ }
256
288
  function isOwnerAnchoredGoalsCapability(value) {
257
289
  return typeof value === 'object' && value !== null
258
290
  && typeof value.inspectOwnerVerifiedWorkflowSource === 'function'
@@ -304,6 +336,19 @@ const configSchema = Schema.object({
304
336
  permissionPickerTtlMs: Schema.number().step(1).min(60_000).max(86_400_000).default(900_000),
305
337
  toolApprovalTtlMs: Schema.number().step(1).min(1_000).max(300_000).default(300_000),
306
338
  toolApprovalPreparationTimeoutMs: Schema.number().step(1).min(100).max(30_000).default(10_000),
339
+ attentionAttachOverCap: Schema.boolean().default(false),
340
+ attentionEnforce: Schema.boolean().default(false),
341
+ ownerReportRouting: Schema.object({ orderAcrossLanes: Schema.boolean().default(false) }),
342
+ naturalFeedback: Schema.object({
343
+ coverage: Schema.boolean().default(false),
344
+ mixedIntent: Schema.boolean().default(false),
345
+ }),
346
+ peerAgents: Schema.array(Schema.object({
347
+ channel: Schema.string().min(1).required(),
348
+ account: Schema.string().min(1).required(),
349
+ tenant: Schema.string().min(1).required(),
350
+ user: Schema.string().min(1).required(),
351
+ })).default([]),
307
352
  ownerRoutes: Schema.array(Schema.object({
308
353
  id: Schema.string().min(1).required(),
309
354
  conversation: Schema.object({
@@ -372,6 +417,8 @@ function pickerIncludesRoute(picker, route) {
372
417
  const model = picker.models.find(candidate => candidate.provider === route.provider && candidate.id === route.model);
373
418
  return model !== undefined && (route.reasoningEffort === undefined || model.effortIds.includes(route.reasoningEffort));
374
419
  }
420
+ /** UTF-16 bound on transient inbound text forwarded to an opted-in intake observer. */
421
+ const MAX_INBOUND_INTAKE_TEXT = 65_536;
375
422
  const MODEL_SELECTION_WAIT_POLL_MS = 500;
376
423
  const MODEL_SELECTION_WAIT_TIMEOUT_MS = 120_000;
377
424
  function isModelSelectionPayload(value) {
@@ -397,6 +444,8 @@ function isModelSelectionPayload(value) {
397
444
  const toolApprovalArgumentBytes = 16 * 1024;
398
445
  const ownerRequestedChangeArgumentBytes = 64 * 1024;
399
446
  const toolApprovalIdentityBytes = 512;
447
+ /** Bounded owner-ordered linked fallback list for one report route. */
448
+ const maxLinkedReportFallback = 8;
400
449
  function inspectUnsettledNativeCall(events, turn, callId, toolName, argumentsJson, argumentBytes = toolApprovalArgumentBytes) {
401
450
  if (!callId || !toolName || Buffer.byteLength(callId) > toolApprovalIdentityBytes
402
451
  || Buffer.byteLength(toolName) > toolApprovalIdentityBytes
@@ -580,6 +629,39 @@ function exactToolApprovalCall(events, openTurn, callId, toolName) {
580
629
  return undefined;
581
630
  return exact;
582
631
  }
632
+ /** Reject peer/caller accessors before any source locks or final local comparisons. */
633
+ function learningSourceData(value) {
634
+ const ancestors = new Set();
635
+ let nodes = 0;
636
+ const visit = (part, depth) => {
637
+ if (part === undefined || part === null || typeof part === 'string' || typeof part === 'boolean' || typeof part === 'number' && Number.isFinite(part))
638
+ return;
639
+ if (!part || typeof part !== 'object' || isProxy(part) || depth > 24 || ancestors.has(part) || ++nodes > 100_000)
640
+ throw new AssistantDeliveryError('runtime-conflict', 'invalid learning source data');
641
+ const array = Array.isArray(part), prototype = Object.getPrototypeOf(part);
642
+ if (prototype !== (array ? Array.prototype : Object.prototype) && prototype !== null)
643
+ throw new AssistantDeliveryError('runtime-conflict', 'invalid learning source prototype');
644
+ ancestors.add(part);
645
+ for (const key of Reflect.ownKeys(part)) {
646
+ const descriptor = Object.getOwnPropertyDescriptor(part, key);
647
+ if (array && key === 'length')
648
+ continue;
649
+ if (typeof key !== 'string' || !descriptor.enumerable || !Object.hasOwn(descriptor, 'value'))
650
+ throw new AssistantDeliveryError('runtime-conflict', 'learning source accessors are not accepted');
651
+ visit(descriptor.value, depth + 1);
652
+ }
653
+ ancestors.delete(part);
654
+ };
655
+ visit(value, 0);
656
+ const freeze = (part) => { if (part && typeof part === 'object') {
657
+ for (const child of Object.values(part))
658
+ freeze(child);
659
+ Object.freeze(part);
660
+ } };
661
+ const copy = structuredClone(value);
662
+ freeze(copy);
663
+ return copy;
664
+ }
583
665
  export class AssistantDeliveryService extends Service {
584
666
  static Config = configSchema;
585
667
  deliveryStore;
@@ -619,14 +701,36 @@ export class AssistantDeliveryService extends Service {
619
701
  preferenceProducerGeneration = `assistant-delivery-preference:${randomUUID()}`;
620
702
  acceptanceProducerGeneration = `assistant-delivery-acceptance:${randomUUID()}`;
621
703
  acceptanceSink;
704
+ workItemReplyConsumer;
705
+ inboundIntakeObserver;
706
+ /** Send-time withdrawal checks for `withdrawable` owner notices, one per (authority, canonical source id). */
707
+ ownerNotificationWithdrawals = new Map();
708
+ /** Live handles issued by `registerOwnerNotificationWithdrawal`, mapped to their (authority, source) key. */
709
+ ownerNotificationWithdrawalHandles = new Map();
710
+ /** Slice-5 reply consumers, one per (authority, canonical owner-notice source id). */
711
+ ownerNotificationReplies = new Map();
622
712
  foregroundObserver;
623
713
  /** Once a live verifier required acceptance, loss of that verifier must not silently bypass it. */
624
714
  acceptanceRequired = false;
625
715
  preferenceTurns = new WeakMap();
716
+ /** Claimed human turns of linked owner aliases; read-only alias inspection only. */
717
+ // Alias turns pin the alias principal row version and link target observed at
718
+ // claim time; a re-link (which bumps the principal version) invalidates them.
719
+ aliasTurns = new WeakMap();
626
720
  goalWakeResults = new WeakMap();
627
721
  modelSelectionFlight;
628
722
  presentationFlight;
629
723
  workflowTraceFlight;
724
+ workItemFeedbackFlight;
725
+ workItemFeedbackCursor = 0;
726
+ workItemFeedbackAckCursor = 0;
727
+ /** WP19 stage 1: audit writes never affect delivery; failures are counted in this process only (reset on restart). */
728
+ attentionAuditWriteFailures = 0;
729
+ /** WP19 stage 2c: process-local progress attention audit (no Delivery schema change). */
730
+ progressAttentionAudit = new ProgressAttentionAudit();
731
+ progressAttentionAuditFailures = 0;
732
+ /** WP19 stage 3: process-local rejection log (rejected notices have no Outbox row). */
733
+ attentionRejections = new AttentionRejectionLog();
630
734
  naturalObjectiveFlight;
631
735
  preferenceProjectionFlight;
632
736
  inboundAdmissionRecoveryFlight;
@@ -721,19 +825,24 @@ export class AssistantDeliveryService extends Service {
721
825
  }
722
826
  ctx.on('agent/inbox/claimed', ({ agent, message, turn }) => {
723
827
  this.capturePreferenceTurn(agent, message.source, turn);
828
+ this.captureAliasTurn(agent, message.source, turn);
724
829
  });
725
830
  ctx.on('agent/inbox/discarded', ({ agent }) => {
726
831
  this.preferenceTurns.delete(agent);
832
+ this.aliasTurns.delete(agent);
727
833
  });
728
834
  ctx.on('agent/status', ({ agent, status }) => {
729
- if (status === 'idle')
835
+ if (status === 'idle') {
730
836
  this.preferenceTurns.delete(agent);
837
+ this.aliasTurns.delete(agent);
838
+ }
731
839
  });
732
840
  this.registry = new DeliveryAdapterRegistry({
733
841
  accept: async (envelope) => {
734
842
  const result = await this.acceptInbound(envelope);
735
843
  return { duplicate: result.duplicate, inboxId: result.inboxId, status: result.status };
736
844
  },
845
+ observe: observation => this.observeInbound(observation),
737
846
  receipt: async (receipt) => {
738
847
  if (!this.active)
739
848
  return;
@@ -843,13 +952,21 @@ export class AssistantDeliveryService extends Service {
843
952
  authorizePermissionReply: (binding, envelope) => this.authorizePermissionReply(binding, envelope),
844
953
  beginModelCommand: conversation => this.deliveryStore.beginModelCommand(conversation),
845
954
  commitModelCommand: input => this.deliveryStore.commitModelCommand(input),
846
- progress: async (binding, eventId, update) => {
955
+ progress: async (binding, eventId, update, origin) => {
956
+ // WP19 stage 2c: audit only; the send path below is unchanged.
957
+ try {
958
+ this.progressAttentionAudit.record({ bindingId: binding.id, eventId, origin, update });
959
+ }
960
+ catch {
961
+ this.progressAttentionAuditFailures += 1;
962
+ }
847
963
  const adapter = this.registry.get(binding.conversation.channel, binding.conversation.account);
848
964
  await adapter?.progress?.({
849
965
  bindingId: binding.id,
850
966
  target: { conversation: binding.conversation, principal: binding.principal },
851
967
  eventId,
852
968
  update,
969
+ origin,
853
970
  });
854
971
  },
855
972
  replyCommand: (binding, eventId, reply, idempotencyKey) => {
@@ -908,6 +1025,9 @@ export class AssistantDeliveryService extends Service {
908
1025
  this.start();
909
1026
  ctx.effect(() => async () => {
910
1027
  this.active = false;
1028
+ // Disarm the scheduler before the first await: the Web owner drain can
1029
+ // take seconds, and an armed tick must not run against a disposed service.
1030
+ this.clearSchedulerTimers();
911
1031
  // The isolated Web owner may already be unwinding concurrently. Await
912
1032
  // its memoized drain while the delivery Store is still available so a
913
1033
  // successfully disposed Agent can record its quiescent lease release.
@@ -916,6 +1036,7 @@ export class AssistantDeliveryService extends Service {
916
1036
  this.preferenceFeedbackSink = undefined;
917
1037
  this.acceptanceSink = undefined;
918
1038
  this.foregroundObserver = undefined;
1039
+ this.workItemReplyConsumer = undefined;
919
1040
  this.evaluationSink = undefined;
920
1041
  this.workflowTraceSink = undefined;
921
1042
  this.automationPresentationBinding?.dispose();
@@ -1531,7 +1652,7 @@ export class AssistantDeliveryService extends Service {
1531
1652
  throw new AssistantDeliveryError('runtime-conflict', 'scheduled goal result exceeds the delivery text limit');
1532
1653
  }
1533
1654
  const situation = goalDefinitionSituation(proof.goal.definitionDigest);
1534
- return this.deliveryStore.enqueueGoalOutcomeTarget({
1655
+ return this.auditAttention('scheduled-goal-result', 'assistant-goals-wake/v1', { lanePurpose: { purpose: 'result' } }, this.deliveryStore.enqueueGoalOutcomeTarget({
1535
1656
  locator, proof,
1536
1657
  intent: {
1537
1658
  idempotencyKey: `goal-outcome:${proof.proofDigest}:${finalBinding.id}`,
@@ -1547,7 +1668,7 @@ export class AssistantDeliveryService extends Service {
1547
1668
  'dsh.learning.proofDigest': proof.proofDigest,
1548
1669
  }),
1549
1670
  },
1550
- });
1671
+ }));
1551
1672
  }
1552
1673
  /** A textless terminal Web wake may notify its original owner, never impersonate a reply. */
1553
1674
  publishScheduledGoalCompletionNotice(input, current, denied) {
@@ -1561,6 +1682,8 @@ export class AssistantDeliveryService extends Service {
1561
1682
  principalVersion: locator.principalVersion, workspace: locator.workspace, preset: locator.preset },
1562
1683
  sessionId: locator.sessionId, idempotencyKey, text: goalWakeCompletionNotice,
1563
1684
  expiresAt: Math.min(input.deadlineAt, proof.receipt.validUntil),
1685
+ // Host-authored result notice: the Host always declares its purpose (WP19).
1686
+ attention: { purpose: 'result' },
1564
1687
  });
1565
1688
  }
1566
1689
  resolveScheduledGoalOutcome(input, current, denied) {
@@ -1979,9 +2102,11 @@ export class AssistantDeliveryService extends Service {
1979
2102
  complete: (handle, input) => this.completeForegroundTaskAcceptance(handle, input),
1980
2103
  beginExecution: (binding, envelope) => this.beginForegroundTaskExecution(binding, envelope),
1981
2104
  completeExecution: (inboxId, input) => this.completeForegroundTaskExecution(inboxId, input),
2105
+ recordExecutionModelSelection: (inboxId, input) => this.deliveryStore.recordForegroundExecutionModelSelection({ inboxId, ...input }),
1982
2106
  claimed: (agent, envelope, turn) => this.capturePreferenceTurn(agent, { kind: 'delivery', channel: envelope.channel, account: envelope.account, eventId: envelope.eventId }, turn),
1983
2107
  notifications: binding => this.deliveryStore.listOutbox({ bindingId: binding.id, limit: 50 }).filter(record => record.status === 'accepted'
1984
- && record.intent.metadata?.['dsh.native-notice'] === 'v1' && this.#notificationGuard(record) === undefined)
2108
+ // Already-accepted notices stay listed: withdrawal only gates the send attempt.
2109
+ && record.intent.metadata?.['dsh.native-notice'] === 'v1' && this.#notificationGuard(record, { withdrawal: false }) === undefined)
1985
2110
  .map(record => ({ id: record.id, text: record.intent.text, createdAt: record.createdAt })),
1986
2111
  }, config);
1987
2112
  this.nativeWebOwner = access;
@@ -1991,11 +2116,17 @@ export class AssistantDeliveryService extends Service {
1991
2116
  capabilities: Object.freeze({ reconcileUnknownSend: true, receipts: [], formats: ['plain'] }),
1992
2117
  async start() { },
1993
2118
  send: async (intent) => {
1994
- if (this.#nativeNoticeGuard(intent) !== undefined)
1995
- return { outcome: 'not-sent', failureCode: 'native-notice-invalid', retryable: false };
2119
+ // Pass the guard verdict through: a withdrawal-unavailable hold must stay retryable.
2120
+ const refused = this.#nativeNoticeGuard(intent);
2121
+ if (refused !== undefined)
2122
+ return refused;
1996
2123
  return { outcome: 'accepted', providerMessageId: this.#notificationProviderId(intent) };
1997
2124
  },
1998
- 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 }; },
2125
+ reconcileUnknownSend: async (record) => {
2126
+ if (record.intent.metadata?.['dsh.native-notice'] !== 'v1')
2127
+ return { outcome: 'not-sent', failureCode: 'native-notice-invalid', retryable: false };
2128
+ return this.#notificationGuard(record) ?? { outcome: 'accepted', providerMessageId: this.#notificationProviderId(record.intent) };
2129
+ },
1999
2130
  };
2000
2131
  ctx.effect(() => {
2001
2132
  const registration = this.registry.register(noticeAdapter);
@@ -2048,6 +2179,25 @@ export class AssistantDeliveryService extends Service {
2048
2179
  return Object.freeze({ protocol: 'assistant-delivery/foreground-tool-call/v1', task,
2049
2180
  turn: turn.turn, call });
2050
2181
  }
2182
+ /** Read-only pages of this exact live owner's persisted human input. Callers
2183
+ * cannot select an Inbox, path, owner, or replacement text through this API. */
2184
+ readOwnerForegroundToolInput(input) {
2185
+ if (!input || typeof input !== 'object' || Reflect.ownKeys(input).some(key => typeof key !== 'string'
2186
+ || !['agent', 'authorityId', 'callId', 'toolName', 'argumentsJson', 'offset', 'maxBytes'].includes(key)))
2187
+ return undefined;
2188
+ const attestation = this.inspectOwnerForegroundToolCall(input);
2189
+ if (attestation === undefined)
2190
+ return undefined;
2191
+ const page = this.deliveryStore.readPendingForegroundToolInput({ task: attestation.task,
2192
+ ...(input.offset === undefined ? {} : { offset: input.offset }), ...(input.maxBytes === undefined ? {} : { maxBytes: input.maxBytes }) });
2193
+ if (page === undefined)
2194
+ return undefined;
2195
+ const current = this.inspectOwnerForegroundToolCall(input);
2196
+ if (current === undefined || acceptanceCanonicalJson(current) !== acceptanceCanonicalJson(attestation))
2197
+ return undefined;
2198
+ return Object.freeze({ protocol: 'assistant-delivery/foreground-tool-input/v1',
2199
+ attestation, ...page });
2200
+ }
2051
2201
  /** Host-only, read-only source proof for an owner's live memory or wiki tool call. */
2052
2202
  inspectOwnerRequestedChange(execution, input) {
2053
2203
  if (!this.active || !execution || typeof execution !== 'object'
@@ -2255,6 +2405,7 @@ export class AssistantDeliveryService extends Service {
2255
2405
  const token = Symbol('assistant-delivery.trusted-evaluation');
2256
2406
  this.evaluationSink = Object.freeze({ token, registration });
2257
2407
  void this.drainNaturalObjectiveIntents();
2408
+ void this.drainWorkItemFeedback().catch(() => { });
2258
2409
  let active = true;
2259
2410
  return () => {
2260
2411
  if (!active)
@@ -2679,6 +2830,16 @@ export class AssistantDeliveryService extends Service {
2679
2830
  return inbox !== undefined && binding !== undefined && inbox.bindingId === binding.id
2680
2831
  && this.isInboundAuthorized(binding, inbox.envelope);
2681
2832
  }
2833
+ /**
2834
+ * Exact owner-configured peer bot, or `undefined`. `senderIsBot` is only the
2835
+ * provider's declaration; the allowlisted tuple is the actual authority.
2836
+ */
2837
+ peerAgentAuthority(envelope) {
2838
+ if (envelope.metadata?.senderIsBot !== 'true')
2839
+ return undefined;
2840
+ return this.config.peerAgents?.find(peer => peer.channel === envelope.channel && peer.account === envelope.account
2841
+ && peer.tenant === envelope.principal.tenant && peer.user === envelope.principal.user);
2842
+ }
2682
2843
  isInboundAuthorized(binding, envelope) {
2683
2844
  if (!this.active)
2684
2845
  return false;
@@ -3113,6 +3274,25 @@ export class AssistantDeliveryService extends Service {
3113
3274
  void this.drainWorkflowTraces();
3114
3275
  return 'recorded';
3115
3276
  }
3277
+ /** Opt-in widened deterministic grammar; off keeps the legacy exact phrases. */
3278
+ widenedNaturalFeedbackCoverage() {
3279
+ return this.config.naturalFeedback?.coverage === true;
3280
+ }
3281
+ /** Splitting one reply into result + continuation requires both opt-ins. */
3282
+ mixedNaturalFeedbackIntent() {
3283
+ return this.config.naturalFeedback?.coverage === true
3284
+ && this.config.naturalFeedback?.mixedIntent === true;
3285
+ }
3286
+ /**
3287
+ * One canonical selection for admission and its later re-derivation fences.
3288
+ * The widened parser is a pure function, so a frozen intent keeps verifying
3289
+ * even after a restart that toggles the opt-in.
3290
+ */
3291
+ naturalObjectiveSelection(text) {
3292
+ return this.widenedNaturalFeedbackCoverage()
3293
+ ? parseNaturalOwnerReply(text)?.status
3294
+ : parseNaturalObjectiveFeedback(text);
3295
+ }
3116
3296
  /**
3117
3297
  * Convert only an authenticated reply to one exact Automation delivery into
3118
3298
  * a trusted objective label. The original run timestamp is retained because
@@ -3121,7 +3301,7 @@ export class AssistantDeliveryService extends Service {
3121
3301
  */
3122
3302
  async dispatchNaturalObjectiveFeedback(binding, envelope) {
3123
3303
  const selection = envelope.kind === 'text' && (envelope.attachments?.length ?? 0) === 0
3124
- ? parseNaturalObjectiveFeedback(envelope.text) : undefined;
3304
+ ? this.naturalObjectiveSelection(envelope.text) : undefined;
3125
3305
  if (selection === undefined || !this.isOwnerFeedbackController(binding, envelope))
3126
3306
  return;
3127
3307
  const replyTarget = envelope.metadata?.replyToProviderMessageId;
@@ -3237,7 +3417,7 @@ export class AssistantDeliveryService extends Service {
3237
3417
  const naturalMatches = naturalIntent !== undefined && inbox.envelope.kind === 'text'
3238
3418
  && naturalIntent.envelopeHash === inbox.envelopeHash
3239
3419
  && naturalIntent.sourceOutboxId === target.id
3240
- && parseNaturalObjectiveFeedback(inbox.envelope.text) === naturalIntent.selection
3420
+ && this.naturalObjectiveSelection(inbox.envelope.text) === naturalIntent.selection
3241
3421
  && (parsed.kind === 'objective-status' || (naturalIntent.status === 'frozen'
3242
3422
  && JSON.stringify(naturalIntent.command) === JSON.stringify(parsed)));
3243
3423
  if ((inbox.envelope.attachments?.length ?? 0) !== 0 || (!typedMatches && !naturalMatches))
@@ -3665,7 +3845,7 @@ export class AssistantDeliveryService extends Service {
3665
3845
  const sessions = ctx.get('sessions');
3666
3846
  const agent = request.agent;
3667
3847
  const events = agent.session.snapshotEvents();
3668
- const reviewer = approvalReviewerOf(events);
3848
+ const reviewer = approvalReviewerOf(events, agent.session.header.version);
3669
3849
  const reviewRoute = reviewer === 'user'
3670
3850
  ? 'user'
3671
3851
  : reviewer === 'auto-review' && ctx.assistantPolicy.isAutoReviewEscalation(request)
@@ -3804,7 +3984,8 @@ export class AssistantDeliveryService extends Service {
3804
3984
  return next();
3805
3985
  let reviewer;
3806
3986
  try {
3807
- reviewer = approvalReviewerOf(request.agent.session.snapshotEvents());
3987
+ const session = request.agent.session;
3988
+ reviewer = approvalReviewerOf(session.snapshotEvents(), session.header.version);
3808
3989
  }
3809
3990
  catch {
3810
3991
  return 'unavailable';
@@ -3983,6 +4164,32 @@ export class AssistantDeliveryService extends Service {
3983
4164
  if (!['received', 'authorized'].includes(record.status)) {
3984
4165
  return { duplicate: accepted.duplicate, inboxId: record.id, status: record.status };
3985
4166
  }
4167
+ // An explicitly allowlisted peer bot enters its own linked-principal lane.
4168
+ // It is still subject to the ordinary Policy ingest decision below and never
4169
+ // receives owner feedback/continuation behavior. The allowlist is re-read
4170
+ // for every provider-declared bot message: removing an entry revokes the
4171
+ // lane even while the old linked principal row is still active.
4172
+ const senderIsBot = envelope.metadata?.senderIsBot === 'true';
4173
+ const peerAuthority = senderIsBot ? this.peerAgentAuthority(envelope) : undefined;
4174
+ if (senderIsBot && peerAuthority === undefined) {
4175
+ record = this.deliveryStore.deadLetterInbox(record.id, 'unauthorized-principal');
4176
+ return { duplicate: accepted.duplicate, inboxId: record.id, status: record.status };
4177
+ }
4178
+ if (peerAuthority !== undefined && !this.deliveryStore.isAuthorizedPrincipal(envelope.principal)) {
4179
+ const owner = this.deliveryStore.activeOwnerPrincipal();
4180
+ if (owner === undefined) {
4181
+ record = this.deliveryStore.deadLetterInbox(record.id, 'peer-owner-unavailable');
4182
+ return { duplicate: accepted.duplicate, inboxId: record.id, status: record.status };
4183
+ }
4184
+ try {
4185
+ this.deliveryStore.ensurePeerPrincipal(envelope.principal, owner.id, { reauthorize: true });
4186
+ }
4187
+ catch (error) {
4188
+ this.context.logger.warn(`assistant-delivery: peer principal admission failed: ${String(error)}`);
4189
+ record = this.deliveryStore.deadLetterInbox(record.id, 'peer-principal-unavailable');
4190
+ return { duplicate: accepted.duplicate, inboxId: record.id, status: record.status };
4191
+ }
4192
+ }
3986
4193
  if (!this.deliveryStore.isAuthorizedPrincipal(envelope.principal)) {
3987
4194
  record = this.deliveryStore.deadLetterInbox(record.id, 'unauthorized-principal');
3988
4195
  return { duplicate: accepted.duplicate, inboxId: record.id, status: record.status };
@@ -4024,6 +4231,7 @@ export class AssistantDeliveryService extends Service {
4024
4231
  beforeInboxId: record.id,
4025
4232
  failureCode: 'new-session-before-dispatch',
4026
4233
  });
4234
+ this.notifyWorkItemReplyCancellation(fence.replyChildItemIds ?? []);
4027
4235
  this.restartStore?.cancelForBinding(binding.id);
4028
4236
  await this.runtime?.cancelActive?.(binding, 'new');
4029
4237
  await this.inbound.cancelUndispatchedClaims(fence.claimedInboxIds, 'new');
@@ -4059,6 +4267,7 @@ export class AssistantDeliveryService extends Service {
4059
4267
  });
4060
4268
  // Holding the transition while the live turn drains prevents a later
4061
4269
  // message from overtaking the durable stop acknowledgement.
4270
+ this.notifyWorkItemReplyCancellation(fence.replyChildItemIds ?? []);
4062
4271
  this.restartStore?.cancelForBinding(binding.id);
4063
4272
  await this.runtime?.cancelActive?.(binding, 'stop');
4064
4273
  await this.inbound.cancelUndispatchedClaims(fence.claimedInboxIds, 'stop');
@@ -4085,132 +4294,1145 @@ export class AssistantDeliveryService extends Service {
4085
4294
  const failed = this.deliveryStore.getInbox(record.id);
4086
4295
  return { duplicate: accepted.duplicate, inboxId: failed.id, status: failed.status };
4087
4296
  }
4297
+ // Slice 5: a reply to the exact delivered owner-question notice is
4298
+ // consumed here, before any Agent admission. Applied replies are
4299
+ // processed; a transient consumer failure leaves the Inbox pending.
4300
+ const attentionReply = this.inspectOwnerQuestionReply(record, binding);
4301
+ if (attentionReply !== 'none') {
4302
+ const current = this.deliveryStore.getInbox(record.id);
4303
+ return { duplicate: accepted.duplicate, inboxId: current.id, status: current.status };
4304
+ }
4305
+ const feedback = this.inspectOwnerWorkItemFeedback(record, binding);
4306
+ const splitContinuation = feedback.kind !== 'none' && feedback.continuation;
4307
+ if (feedback.kind === 'ready') {
4308
+ // One reply may carry a result and an explicit continuation. They are two
4309
+ // independently fenced decisions: admit both atomically when both lanes
4310
+ // are permitted, otherwise record the result alone and record the reason.
4311
+ let admittedContinuation = false;
4312
+ if (splitContinuation) {
4313
+ const report = this.prepareWorkItemReply(record, binding);
4314
+ if (report !== undefined) {
4315
+ try {
4316
+ this.deliveryStore.admitOwnerWorkItemFeedbackWithReply({ feedback: feedback.source, binding, report, fence: feedback.fence });
4317
+ admittedContinuation = true;
4318
+ }
4319
+ catch (error) {
4320
+ // The combined transaction rolled back; fall back to the result lane
4321
+ // only. A failed result fence re-throws on that fallback, as before.
4322
+ this.context.logger.warn(`assistant-delivery: mixed owner feedback continuation admission failed: ${String(error)}`);
4323
+ }
4324
+ }
4325
+ }
4326
+ if (!admittedContinuation) {
4327
+ this.deliveryStore.workItemFeedback.admit(feedback.source, feedback.fence);
4328
+ if (splitContinuation)
4329
+ this.recordPartialMixedFeedbackAdmission(record, 'continuation-not-admitted');
4330
+ }
4331
+ if (admittedContinuation)
4332
+ this.notifyWorkItemReplyPending();
4333
+ void this.drainWorkItemFeedback().catch(() => { });
4334
+ return { duplicate: accepted.duplicate, inboxId: record.id, status: 'processed' };
4335
+ }
4336
+ // A pure recognized result never becomes execution when feedback permission
4337
+ // is absent; only an explicit continuation in the same reply may proceed.
4338
+ const continuationAllowed = feedback.kind === 'none'
4339
+ || (feedback.kind === 'ordinary' && splitContinuation);
4340
+ if (continuationAllowed && this.admitWorkItemReply(record, binding)) {
4341
+ const handedOff = this.deliveryStore.getInbox(record.id);
4342
+ this.notifyWorkItemReplyPending();
4343
+ if (feedback.kind === 'ordinary')
4344
+ this.recordPartialMixedFeedbackAdmission(record, 'feedback-not-admitted');
4345
+ return { duplicate: accepted.duplicate, inboxId: handedOff.id, status: handedOff.status };
4346
+ }
4347
+ if (feedback.kind === 'ordinary' && splitContinuation)
4348
+ this.recordPartialMixedFeedbackAdmission(record, 'neither-lane-admitted');
4088
4349
  record = this.deliveryStore.queueInbox(record.id, binding.id);
4089
4350
  return { duplicate: accepted.duplicate, inboxId: record.id, status: record.status };
4090
4351
  });
4091
4352
  }
4092
- enqueueBackground(input) {
4093
- this.assertActive();
4094
- if (containsReservedLearningMetadata(input.metadata) || Object.keys(input.metadata ?? {}).some(key => key.startsWith('dsh.native-notice'))) {
4095
- throw new AssistantDeliveryError('runtime-conflict', 'reserved metadata requires its typed delivery interface');
4353
+ /**
4354
+ * Deterministic backend classification of one authenticated owner reply to an
4355
+ * exact report. The legacy closed whole-sentence grammar stays in force unless
4356
+ * the widened coverage opt-in is on; no caller text is reinterpreted as an
4357
+ * instruction, and no model is called.
4358
+ */
4359
+ parseWorkItemFeedback(envelope) {
4360
+ if ((envelope.attachments?.length ?? 0) !== 0)
4361
+ return undefined;
4362
+ if (envelope.kind === 'text') {
4363
+ const text = envelope.text.trim();
4364
+ if (!this.widenedNaturalFeedbackCoverage()) {
4365
+ if (!/^(?:问题解决了|已经解决了|现在解决了|还是不行|还没解决|问题还没解决|撤回刚才的任务反馈|撤回我刚才的任务反馈)[。.!!]?$/u.test(text))
4366
+ return undefined;
4367
+ const value = parseNaturalObjectiveFeedback(text);
4368
+ return value === undefined ? undefined : { selection: { kind: 'natural', value }, continuation: false };
4369
+ }
4370
+ const parsed = parseNaturalOwnerReply(text);
4371
+ return parsed === undefined ? undefined : {
4372
+ selection: { kind: 'natural', value: parsed.status },
4373
+ continuation: parsed.continuation && this.mixedNaturalFeedbackIntent(),
4374
+ };
4096
4375
  }
4097
- return this.enqueueBackgroundAuthorized(input);
4376
+ const command = parseDeliveryCommand(envelope);
4377
+ if (command?.name !== 'feedback')
4378
+ return undefined;
4379
+ const parsed = parseFeedbackCommand(command.rawInput);
4380
+ return ['objective', 'objective-status', 'objective-revision'].includes(parsed.kind)
4381
+ ? { selection: { kind: 'explicit', command: parsed }, continuation: false }
4382
+ : undefined;
4098
4383
  }
4099
- enqueueBackgroundAuthorized(input) {
4100
- const binding = this.deliveryStore.getBinding(input.bindingId);
4101
- if (binding === undefined || binding.status !== 'active' || !this.hasActivePrincipal(binding)) {
4102
- throw new AssistantDeliveryError('missing-binding', 'delivery binding does not exist or is revoked');
4103
- }
4104
- if (binding.workspace !== input.workspace) {
4105
- throw new AssistantDeliveryError('missing-binding', 'delivery workspace does not match the active binding');
4106
- }
4107
- const decision = this.policy.authorize({ subject: { kind: 'background', id: input.sourceId,
4108
- workspace: input.workspace, principal: externalPrincipalId(binding.principal) }, action: 'send',
4109
- resource: { kind: 'message', id: binding.id }, context: { initiator: 'background' } }, { idempotencyKey: `message-send:${input.idempotencyKey}` });
4110
- if (decision.effect !== 'allow')
4111
- throw policyDenied(decision);
4112
- if (!this.hasActivePrincipal(binding)) {
4113
- throw new AssistantDeliveryError('missing-binding', 'delivery binding principal is no longer active');
4384
+ workItemFeedbackPermission(binding, registration) {
4385
+ return this.policy.evaluate({ subject: { kind: 'external', id: externalPrincipalId(binding.principal), workspace: binding.workspace, principal: externalPrincipalId(binding.principal) },
4386
+ action: 'feedback-work-items', resource: { kind: 'automation', id: registration.continuationAutomationId }, context: { initiator: 'external' } }).effect === 'allow';
4387
+ }
4388
+ /**
4389
+ * Inspect one Inbox as an authenticated owner result reply to an exact report.
4390
+ * `ready` carries the frozen source plus its live admission fence; `ordinary`
4391
+ * keeps the historical "recognized result but separate feedback permission
4392
+ * absent" outcome and never admits the result lane by itself. The
4393
+ * `continuation` flag is a deterministic classification only.
4394
+ */
4395
+ inspectOwnerWorkItemFeedback(inbox, binding) {
4396
+ const registration = this.currentWorkItemReplyConsumer();
4397
+ const parsed = this.parseWorkItemFeedback(inbox.envelope);
4398
+ if (!registration || registration.feedbackEnabled !== true || parsed === undefined || !inbox.envelope.metadata?.replyToProviderMessageId)
4399
+ return { kind: 'none' };
4400
+ const report = this.deliveryStore.workItemReplies.replyReport(inbox.channel, inbox.account, inbox.envelope.metadata.replyToProviderMessageId);
4401
+ if (!report || !['accepted', 'delivered', 'read'].includes(report.outbox.status) || !report.outbox.providerMessageId
4402
+ || workItemReplyCanonical(report.outbox.intent.target.conversation) !== workItemReplyCanonical(inbox.envelope.conversation)
4403
+ || workItemReplyCanonical(report.outbox.intent.target.principal) !== workItemReplyCanonical(binding.principal))
4404
+ return { kind: 'none' };
4405
+ let target;
4406
+ try {
4407
+ target = this.inspectWorkItemReport({ sourceId: registration.sourceId, itemId: report.target.itemId, reportRevision: report.target.reportRevision,
4408
+ bindingId: report.outbox.intent.bindingId, idempotencyKey: report.outbox.intent.idempotencyKey, text: report.outbox.intent.text,
4409
+ format: report.outbox.intent.format === 'markdown' ? 'markdown' : 'plain' }, true);
4114
4410
  }
4115
- return this.deliveryStore.enqueue({ idempotencyKey: input.idempotencyKey, bindingId: binding.id,
4116
- target: { conversation: binding.conversation, principal: binding.principal }, text: input.text,
4117
- format: input.format ?? 'plain',
4118
- ...(input.metadata === undefined ? {} : { metadata: input.metadata }) });
4411
+ catch {
4412
+ return { kind: 'none' };
4413
+ }
4414
+ if (workItemReplyCanonical(target) !== workItemReplyCanonical(report.target))
4415
+ return { kind: 'none' };
4416
+ const continuation = parsed.continuation;
4417
+ // A recognized result must not be converted into execution when its separate feedback permission is absent.
4418
+ if (!this.workItemFeedbackPermission(binding, registration))
4419
+ return { kind: 'ordinary', continuation };
4420
+ const source = { protocol: 'assistant-delivery/work-item-feedback/v1', inboxId: inbox.id, envelopeHash: inbox.envelopeHash,
4421
+ inputText: inbox.envelope.text, inputSha256: createHash('sha256').update(inbox.envelope.text).digest('hex'), admissionCursor: inbox.admissionCursor, occurredAt: inbox.receivedAt,
4422
+ owner: target.owner, principalId: externalPrincipalId(binding.principal), binding: { id: binding.id, version: binding.version, generation: binding.generation }, scope: target.scope,
4423
+ target: { outboxId: report.outbox.id, providerMessageId: report.outbox.providerMessageId, itemId: target.itemId, reportRevision: target.reportRevision, reportDigest: target.reportDigest, streamId: target.streamId }, selection: parsed.selection,
4424
+ operationId: `owner-work-item-feedback:${createHash('sha256').update(JSON.stringify([inbox.id, inbox.envelopeHash])).digest('hex')}` };
4425
+ const fence = () => {
4426
+ const current = this.currentWorkItemReplyConsumer(), owner = this.ownerLineageForBinding(binding), active = this.deliveryStore.getBinding(binding.id);
4427
+ if (current !== registration || current?.feedbackEnabled !== true || workItemReplyCanonical(owner) !== workItemReplyCanonical(source.owner)
4428
+ || workItemReplyCanonical(active) !== workItemReplyCanonical(binding) || !this.workItemFeedbackPermission(binding, registration))
4429
+ throw new Error('feedback admission authority changed');
4430
+ };
4431
+ return { kind: 'ready', source, fence, continuation };
4432
+ }
4433
+ /** One deterministic, non-durable record when a mixed reply could only admit one lane. */
4434
+ recordPartialMixedFeedbackAdmission(inbox, reason) {
4435
+ this.context.logger.warn(`assistant-delivery: mixed owner feedback ${inbox.id} admitted partially (${reason})`);
4436
+ }
4437
+ /** Passing an explicit registration makes this the callback-free final local authority check. */
4438
+ workItemFeedbackFence(intent, recorded = false, expectedRegistration) {
4439
+ const registration = expectedRegistration ?? this.currentWorkItemReplyConsumer(), source = intent.source;
4440
+ const current = this.deliveryStore.workItemFeedback.get(source.inboxId);
4441
+ const inbox = this.deliveryStore.getInbox(source.inboxId), original = this.deliveryStore.getBinding(source.binding.id);
4442
+ const binding = recorded && original ? this.deliveryStore.getActiveBinding(original.conversation) : original;
4443
+ if (!this.active || !registration || this.workItemReplyConsumer !== registration || registration.feedbackEnabled !== true || !current || current.state === 'cancelled' || (current.state === 'conflict' && !recorded)
4444
+ || current.state !== intent.state || current.sourceDigest !== intent.sourceDigest || !inbox || inbox.status !== 'processed' || inbox.bindingId !== source.binding.id
4445
+ || inbox.envelopeHash !== source.envelopeHash || inbox.envelope.text !== source.inputText || !binding || binding.status !== 'active'
4446
+ || (!recorded && (binding.id !== source.binding.id || binding.version !== source.binding.version || binding.generation !== source.binding.generation))
4447
+ || workItemReplyCanonical(this.ownerLineageForBinding(binding)) !== workItemReplyCanonical(source.owner)
4448
+ || binding.workspace !== source.scope.workspace || binding.agentPreset !== source.scope.preset
4449
+ || !this.workItemFeedbackPermission(binding, registration))
4450
+ throw new Error('work item feedback authority changed');
4451
+ return registration;
4452
+ }
4453
+ workItemFeedbackProof(intent, recorded = false) {
4454
+ const registration = this.workItemFeedbackFence(intent, recorded), source = intent.source;
4455
+ const report = this.deliveryStore.workItemReplies.replyReport(this.deliveryStore.getInbox(source.inboxId).channel, this.deliveryStore.getInbox(source.inboxId).account, source.target.providerMessageId);
4456
+ if (!report || report.outbox.id !== source.target.outboxId || !['accepted', 'delivered', 'read'].includes(report.outbox.status))
4457
+ throw new Error('work item feedback report changed');
4458
+ const target = this.inspectWorkItemReport({ sourceId: registration.sourceId, itemId: source.target.itemId, reportRevision: source.target.reportRevision,
4459
+ bindingId: report.outbox.intent.bindingId, idempotencyKey: report.outbox.intent.idempotencyKey, text: report.outbox.intent.text, format: report.outbox.intent.format === 'markdown' ? 'markdown' : 'plain' }, true);
4460
+ if (workItemReplyCanonical(target) !== workItemReplyCanonical(report.target))
4461
+ throw new Error('work item feedback target changed');
4462
+ this.workItemFeedbackFence(intent, recorded);
4463
+ const peer = this.context.get('assistantWorkItems', false);
4464
+ if (!peer?.issueOwnerWorkItemFeedbackTarget || !peer.resolveOwnerWorkItemFeedbackTarget)
4465
+ return undefined;
4466
+ const capability = peer.issueOwnerWorkItemFeedbackTarget({ itemId: source.target.itemId, reportRevision: source.target.reportRevision, reportDigest: source.target.reportDigest });
4467
+ this.workItemFeedbackFence(intent, recorded);
4468
+ if (capability === undefined)
4469
+ return undefined;
4470
+ const proof = peer.resolveOwnerWorkItemFeedbackTarget(capability);
4471
+ if (workItemPeerIdentity(this.context.get('assistantWorkItems', false)) !== workItemPeerIdentity(peer) || this.workItemFeedbackFence(intent, recorded, registration) !== registration)
4472
+ return undefined;
4473
+ if (!proof)
4474
+ return undefined;
4475
+ if (proof.protocol !== 'assistant-work-items/owner-report-feedback/v1' || proof.itemId !== source.target.itemId || proof.reportRevision !== source.target.reportRevision
4476
+ || proof.reportDigest !== source.target.reportDigest || proof.sourceId !== registration.sourceId || proof.scope.workspace !== source.scope.workspace || proof.scope.preset !== source.scope.preset
4477
+ || proof.owner.principalRecordId !== source.owner.principalRecordId || proof.owner.principalVersion !== source.owner.principalVersion || proof.owner.principalId !== source.principalId
4478
+ || proof.originBindingId !== report.binding.id || proof.production.quiescent !== true || proof.production.budgetSettlementState !== 'finalized'
4479
+ || !/^[a-f0-9]{64}$/u.test(proof.proofDigest))
4480
+ throw new Error('work item feedback proof mismatch');
4481
+ const { proofDigest: observedDigest, ...unsignedProof } = proof;
4482
+ if (workItemReplyDigest(unsignedProof) !== observedDigest)
4483
+ throw new Error('noncanonical work item feedback proof');
4484
+ if (intent.proofDigest !== undefined && intent.proofDigest !== proof.proofDigest)
4485
+ throw new Error('frozen work item feedback proof changed');
4486
+ return { capability, proof, registration, peerIdentity: workItemPeerIdentity(peer) };
4487
+ }
4488
+ drainWorkItemFeedback() {
4489
+ if (this.workItemFeedbackFlight)
4490
+ return this.workItemFeedbackFlight;
4491
+ const flight = this.runWorkItemFeedback().finally(() => { if (this.workItemFeedbackFlight === flight)
4492
+ this.workItemFeedbackFlight = undefined; });
4493
+ this.workItemFeedbackFlight = flight;
4494
+ return flight;
4119
4495
  }
4120
- #notificationGuard(record) {
4121
- const key = record.intent.idempotencyKey;
4122
- if (key.startsWith('inbound:') && (key.endsWith(':maintenance') || key.endsWith(':maintenance-failed'))) {
4496
+ async runWorkItemFeedback() {
4497
+ if (!this.active || this.currentWorkItemReplyConsumer()?.feedbackEnabled !== true)
4498
+ return;
4499
+ this.drainWorkItemFeedbackAcks();
4500
+ if (!this.evaluationSink)
4501
+ return;
4502
+ const page = this.deliveryStore.workItemFeedback.pending(this.workItemFeedbackCursor, 100);
4503
+ this.workItemFeedbackCursor = page.length === 100 ? page.at(-1).source.admissionCursor.sequence : 0;
4504
+ for (let intent of page) {
4505
+ if (!this.active)
4506
+ return;
4123
4507
  try {
4124
- const failed = key.endsWith(':maintenance-failed');
4125
- const inboxId = key.slice('inbound:'.length, -(failed ? ':maintenance-failed'.length : ':maintenance'.length));
4126
- const request = this.restartStore?.getRequestForTask(inboxId);
4127
- const task = this.restartStore?.getTask(inboxId);
4128
- const inbox = this.deliveryStore.getInbox(inboxId);
4129
- const binding = this.deliveryStore.getBinding(record.intent.bindingId);
4130
- if (request === undefined || task === undefined || inbox === undefined || binding === undefined
4131
- || request.status === 'cancelled' || (failed ? request.status !== 'failed' : !this.restartAuthorized(request))
4132
- || task.binding.id !== binding.id || task.binding.version !== binding.version || task.binding.generation !== binding.generation
4133
- || task.owner.principalRecordId !== this.ownerLineageForBinding(binding)?.principalRecordId
4134
- || task.owner.principalVersion !== this.ownerLineageForBinding(binding)?.principalVersion
4135
- || inbox.bindingId !== binding.id || inbox.eventId !== record.intent.replyToEventId
4136
- || !this.isInboundAuthorized(binding, inbox.envelope)
4137
- || this.deliveryStore.getOutboxByIdempotencyKey(`inbound:${inbox.id}:reply`) !== undefined
4138
- || this.deliveryStore.getOutboxByIdempotencyKey(`inbound:${inbox.eventId}:reply`) !== undefined)
4139
- throw new Error('maintenance notice is stale');
4140
- const decision = this.policy.evaluate({ subject: { kind: 'agent', id: binding.agentPreset,
4141
- workspace: binding.workspace, principal: externalPrincipalId(binding.principal) },
4142
- action: 'reply', resource: { kind: 'message', id: binding.id }, context: { initiator: 'external' } });
4143
- if (decision.effect !== 'allow')
4144
- throw new Error('maintenance notice permission changed');
4508
+ const resolved = this.workItemFeedbackProof(intent);
4509
+ if (!resolved)
4510
+ continue;
4511
+ const { proof, capability } = resolved;
4512
+ const sink = this.evaluationSink;
4513
+ if (!sink || sink.registration.ownerRevisionProtocol !== 'owner-objective-revision/v2' || !sink.registration.inspect)
4514
+ continue;
4515
+ const source = intent.source, initialKey = `work-item-owner-initial:${workItemReplyDigest([source.scope, source.target.itemId, source.owner])}`;
4516
+ const claimsFor = (command) => ({ scope: source.scope, situation: `work-item:${proof.itemKind}`,
4517
+ subjectKind: 'work-item', subjectRef: source.target.itemId, runId: proof.production.runId, outboxId: source.target.outboxId, chatId: this.deliveryStore.getInbox(source.inboxId).envelope.conversation.chat,
4518
+ principalId: source.principalId, bindingId: source.binding.id, workItemOutcomeCapability: capability,
4519
+ objectiveStatus: command.kind === 'objective-status' ? 'achieved' : command.objectiveStatus,
4520
+ ownerCommand: { ...source.owner, operationId: source.operationId, action: command.kind === 'objective-revision' ? command.action : 'initial', ...(command.kind === 'objective-revision' ? { expectedVersion: command.expectedVersion, previousStatus: command.previousStatus } : {}) },
4521
+ occurredAt: source.occurredAt, initialIdempotencyKey: initialKey, idempotencyKey: command.kind === 'objective-revision' ? source.operationId : initialKey });
4522
+ const inspectClaims = claimsFor({ kind: 'objective-status' }), inspectCapability = sink.registration.issueCapability(inspectClaims);
4523
+ if (this.evaluationSink?.token !== sink.token)
4524
+ continue;
4525
+ this.workItemFeedbackFence(intent);
4526
+ const current = sink.registration.inspect(inspectCapability);
4527
+ this.workItemFeedbackFence(intent);
4528
+ if (this.evaluationSink?.token !== sink.token)
4529
+ continue;
4530
+ const evaluation = this.context.get('assistantEvaluation', false);
4531
+ const projection = evaluation?.getTrustedWorkItemLearningProjection({ scope: evaluation.canonicalHostScope(source.scope), itemId: source.target.itemId });
4532
+ this.workItemFeedbackFence(intent);
4533
+ if (intent.state === 'pending') {
4534
+ const selection = source.selection;
4535
+ let command;
4536
+ if (selection.kind === 'explicit')
4537
+ command = selection.command;
4538
+ else if (selection.value === 'withdraw' && (!current || current.objectiveStatus === 'unknown')) {
4539
+ this.deliveryStore.workItemFeedback.settle(source.inboxId, 'conflict', undefined, () => { this.workItemFeedbackFence(intent); });
4540
+ continue;
4541
+ }
4542
+ else if (current && selection.value === current.objectiveStatus)
4543
+ command = { kind: 'objective-status' };
4544
+ else
4545
+ command = current ? { kind: 'objective-revision', action: selection.value === 'withdraw' ? 'withdraw' : 'correct', expectedVersion: current.version, previousStatus: current.objectiveStatus, objectiveStatus: selection.value === 'withdraw' ? 'unknown' : selection.value }
4546
+ : { kind: 'objective', objectiveStatus: selection.value };
4547
+ intent = this.deliveryStore.workItemFeedback.freeze(source.inboxId, command, proof.proofDigest, () => { this.workItemFeedbackFence(intent); });
4548
+ }
4549
+ if (intent.state !== 'frozen' || !intent.command)
4550
+ continue;
4551
+ const command = intent.command;
4552
+ if (command.kind === 'objective-status') {
4553
+ this.deliveryStore.workItemFeedback.settle(source.inboxId, 'recorded', { version: current?.version ?? 0, objectiveStatus: current?.objectiveStatus ?? 'unknown', ...(projection?.objective?.outcomeId ? { outcomeId: projection.objective.outcomeId } : {}) }, () => { this.workItemFeedbackFence(intent); });
4554
+ continue;
4555
+ }
4556
+ const claims = claimsFor(command), receipt = sink.registration.issueCapability(claims);
4557
+ this.workItemFeedbackFence(intent);
4558
+ if (this.evaluationSink?.token !== sink.token)
4559
+ continue;
4560
+ const outcome = sink.registration.append({ capabilityReceipt: receipt, runId: claims.runId, outboxId: claims.outboxId, chatId: claims.chatId, principalId: claims.principalId, bindingId: claims.bindingId, idempotencyKey: claims.idempotencyKey });
4561
+ const settled = this.deliveryStore.workItemFeedback.get(source.inboxId);
4562
+ if (this.evaluationSink?.token !== sink.token || !outcome.ownerFeedbackState || settled?.state !== 'recorded'
4563
+ || settled.canonical?.outcomeId !== outcome.id)
4564
+ continue;
4565
+ this.workItemFeedbackFence(settled);
4145
4566
  }
4146
- catch {
4147
- return { outcome: 'not-sent', failureCode: 'maintenance-notice-stale', retryable: false };
4567
+ catch (error) {
4568
+ if (typeof error === 'object' && error !== null && 'code' in error && ['version-conflict', 'idempotency-conflict'].includes(String(error.code))) {
4569
+ this.deliveryStore.workItemFeedback.settle(intent.source.inboxId, 'conflict', undefined, () => { this.workItemFeedbackFence(intent); });
4570
+ }
4571
+ // Dependency absence and unknown append outcomes remain durable; no foreground or worker replay.
4148
4572
  }
4149
- return undefined;
4150
4573
  }
4151
- if (key.startsWith('inbound:') && key.endsWith(':interrupted')) {
4574
+ if (this.active)
4575
+ this.drainWorkItemFeedbackAcks();
4576
+ }
4577
+ drainWorkItemFeedbackAcks() {
4578
+ const page = this.deliveryStore.workItemFeedback.pendingAcknowledgements(this.workItemFeedbackAckCursor, 100);
4579
+ this.workItemFeedbackAckCursor = page.length === 100 ? page.at(-1).source.admissionCursor.sequence : 0;
4580
+ for (const intent of page) {
4152
4581
  try {
4153
- const inboxId = key.slice('inbound:'.length, -':interrupted'.length);
4154
- const inbox = this.deliveryStore.getInbox(inboxId);
4155
- const binding = this.deliveryStore.getBinding(record.intent.bindingId);
4156
- const expectedNotice = inbox?.failureCode === 'agent-task-timeout'
4157
- ? timedOutTurnNotice : interruptedTurnNotice;
4158
- if (record.intent.text !== expectedNotice || inbox === undefined || binding === undefined
4159
- || !this.deliveryStore.canSendInterruptedInboxNotice(record)
4160
- || !this.isInboundAuthorized(binding, inbox.envelope)) {
4161
- throw new Error('interrupted Inbox authority changed');
4162
- }
4163
- const reply = this.policy.evaluate({
4164
- subject: { kind: 'agent', id: binding.agentPreset, workspace: binding.workspace,
4165
- principal: externalPrincipalId(binding.principal) },
4166
- action: 'reply', resource: { kind: 'message', id: binding.id },
4167
- context: { initiator: 'external' },
4168
- });
4169
- if (reply.effect !== 'allow')
4170
- throw new Error('interrupted Inbox reply authority changed');
4171
- }
4172
- catch {
4173
- return { outcome: 'not-sent', failureCode: 'interrupted-inbox-authority-revoked', retryable: false };
4582
+ const registration = this.workItemFeedbackFence(intent, true);
4583
+ const original = this.deliveryStore.getBinding(intent.source.binding.id), binding = this.deliveryStore.getActiveBinding(original.conversation);
4584
+ const text = intent.state === 'conflict' ? '任务反馈版本已变化,本次更改未生效。请回复原报告发送 /feedback status 查看当前版本。'
4585
+ : intent.canonical?.version === 0 ? '查询时该工作项暂无已记录反馈。' : objectiveStateMessage(intent.canonical).replace('当前任务反馈:', intent.command?.kind === 'objective-status' ? '查询时的任务反馈:' : '本次已记录任务反馈:');
4586
+ this.enqueueBackgroundAuthorized({ sourceId: registration.sourceId, workspace: binding.workspace, bindingId: binding.id,
4587
+ idempotencyKey: `feedback:${intent.source.inboxId}:ack`, text, format: 'plain' }, undefined,
4588
+ // Host-authored result ACK: the Host always declares its purpose through the lane (WP19).
4589
+ { lane: 'background', lanePurpose: { purpose: 'result' } });
4174
4590
  }
4591
+ catch { /* Recording is independent of send authority; only an absent Outbox is retried. */ }
4592
+ }
4593
+ }
4594
+ withOwnerWorkItemFeedbackWriterFence(registration, claims, proofDigest, callback) {
4595
+ this.assertActive();
4596
+ const sink = this.evaluationSink;
4597
+ if (!sink || sink.registration !== registration || !registrationOwnedByEvaluation(registration)
4598
+ || claims.subjectKind !== 'work-item' || !claims.ownerCommand)
4599
+ throw new Error('work item owner writer registration is not current');
4600
+ const consumer = this.currentWorkItemReplyConsumer();
4601
+ const peer = this.context.get('assistantWorkItems', false);
4602
+ const peerIdentity = workItemPeerIdentity(peer);
4603
+ if (!consumer || consumer.feedbackEnabled !== true || typeof peer?.resolveOwnerWorkItemFeedbackTarget !== 'function')
4604
+ throw new Error('work item feedback producer is not current');
4605
+ return this.deliveryStore.workItemFeedback.withOwnerWriterFence(claims.ownerCommand.operationId, intent => {
4606
+ const source = intent.source, command = intent.command;
4607
+ const initialKey = `work-item-owner-initial:${workItemReplyDigest([source.scope, source.target.itemId, source.owner])}`;
4608
+ if (claims.initialIdempotencyKey !== initialKey || claims.idempotencyKey !== (command?.kind === 'objective-revision' ? source.operationId : initialKey))
4609
+ throw new Error('owner feedback operation identity changed');
4610
+ if (!command || command.kind === 'objective-status' || intent.proofDigest !== proofDigest || source.operationId !== claims.ownerCommand.operationId
4611
+ || workItemReplyCanonical(source.scope) !== workItemReplyCanonical(claims.scope) || source.target.itemId !== claims.subjectRef
4612
+ || source.target.outboxId !== claims.outboxId || source.binding.id !== claims.bindingId || source.principalId !== claims.principalId
4613
+ || claims.chatId !== this.deliveryStore.getInbox(source.inboxId).envelope.conversation.chat || claims.occurredAt !== source.occurredAt
4614
+ || claims.objectiveStatus !== command.objectiveStatus
4615
+ || workItemReplyCanonical(claims.ownerCommand) !== workItemReplyCanonical({ ...source.owner, operationId: source.operationId,
4616
+ action: command.kind === 'objective-revision' ? command.action : 'initial', ...(command.kind === 'objective-revision' ? { expectedVersion: command.expectedVersion, previousStatus: command.previousStatus } : {}) }))
4617
+ throw new Error('frozen work item feedback claims changed');
4618
+ // Complete every ownership callback before one final bounded production
4619
+ // resolve. No producer callback runs between that proof and the write.
4620
+ if (this.currentWorkItemReplyConsumer() !== consumer || this.evaluationSink?.token !== sink.token)
4621
+ throw new Error('feedback writer generation changed');
4622
+ const finalProof = peer.resolveOwnerWorkItemFeedbackTarget(claims.workItemOutcomeCapability);
4623
+ if (!finalProof || finalProof.proofDigest !== proofDigest || workItemPeerIdentity(this.context.get('assistantWorkItems', false)) !== peerIdentity
4624
+ || this.evaluationSink?.token !== sink.token)
4625
+ throw new Error('production proof changed after writer ownership callbacks');
4626
+ const { proofDigest: finalDigest, ...finalUnsigned } = finalProof;
4627
+ if (workItemReplyDigest(finalUnsigned) !== finalDigest)
4628
+ throw new Error('final production proof digest changed');
4629
+ this.workItemFeedbackFence(intent, false, consumer);
4630
+ return callback();
4631
+ }, (intent, result) => {
4632
+ const value = result;
4633
+ const command = intent.command;
4634
+ if (!value || typeof value !== 'object' || !value.id || !value.ownerFeedbackState || command.kind === 'objective-status'
4635
+ || value.ownerFeedbackState.objectiveStatus !== claims.objectiveStatus
4636
+ || value.ownerFeedbackState.version !== (command.kind === 'objective-revision' ? command.expectedVersion + 1 : 1)
4637
+ || value.objectiveStatus !== claims.objectiveStatus || value.idempotencyKey !== claims.idempotencyKey
4638
+ || workItemReplyCanonical(value.scope) !== workItemReplyCanonical(claims.scope)
4639
+ || value.source?.kind !== 'user-feedback' || value.source.id !== 'assistant-delivery/typed-owner-feedback'
4640
+ || !value.evidence?.some(ref => ref.kind === 'work-item-owner-result' && ref.ref === intent.source.target.itemId)
4641
+ || !value.evidence?.some(ref => ref.kind === 'work-item-production-proof' && ref.ref === claims.runId && ref.digest === proofDigest))
4642
+ throw new Error('canonical work item feedback result changed');
4643
+ if (this.evaluationSink?.token !== sink.token)
4644
+ throw new Error('feedback writer registration changed before ACK');
4645
+ this.workItemFeedbackFence(intent, false, consumer);
4646
+ return { ...value.ownerFeedbackState, outcomeId: value.id };
4647
+ });
4648
+ }
4649
+ workItemReplyProtocolVersion() { return 1; }
4650
+ currentWorkItemReplyConsumer() {
4651
+ const registration = this.workItemReplyConsumer;
4652
+ const peer = this.context.get('assistantWorkItems', false);
4653
+ return this.active && registration !== undefined && peer?.ownsWorkItemReplyRegistration?.(registration) === true ? registration : undefined;
4654
+ }
4655
+ registerWorkItemReplyConsumer(registration) {
4656
+ this.assertActive();
4657
+ if (!registration || registration.protocol !== 1 || typeof registration.sourceId !== 'string' || !registration.sourceId
4658
+ || typeof registration.continuationAutomationId !== 'string' || !registration.continuationAutomationId
4659
+ || ['inspectReport', 'inspectChild', 'onPending', 'onCancelled'].some(key => typeof registration[key] !== 'function')) {
4660
+ throw new AssistantDeliveryError('runtime-conflict', 'invalid work item reply consumer');
4661
+ }
4662
+ if (this.workItemReplyConsumer !== undefined)
4663
+ throw new AssistantDeliveryError('runtime-conflict', 'work item reply consumer already registered');
4664
+ this.workItemReplyConsumer = registration;
4665
+ if (this.currentWorkItemReplyConsumer() !== registration) {
4666
+ this.workItemReplyConsumer = undefined;
4667
+ throw new AssistantDeliveryError('runtime-conflict', 'work item reply consumer lacks current producer ownership');
4668
+ }
4669
+ let live = true;
4670
+ // Registered callbacks run synchronously and only receive committed state.
4671
+ // The consumer schedules its own bounded drain; Delivery owns no extra loop.
4672
+ this.notifyWorkItemReplyPending();
4673
+ void this.drainWorkItemFeedback().catch(() => { });
4674
+ return () => { if (live) {
4675
+ live = false;
4676
+ if (this.workItemReplyConsumer === registration)
4677
+ this.workItemReplyConsumer = undefined;
4678
+ } };
4679
+ }
4680
+ /**
4681
+ * WP20 intake: one observer, owned by the current WorkItems service, receives
4682
+ * a metadata-only signal per adapter observation. Delivery adds only the
4683
+ * read-only allowlist and tracked-report-thread facts; it writes nothing.
4684
+ */
4685
+ registerInboundIntakeObserver(observer) {
4686
+ this.assertActive();
4687
+ if (!observer || observer.protocol !== 1 || typeof observer.observe !== 'function')
4688
+ throw new AssistantDeliveryError('runtime-conflict', 'invalid inbound intake observer');
4689
+ if (this.inboundIntakeObserver !== undefined)
4690
+ throw new AssistantDeliveryError('runtime-conflict', 'inbound intake observer already registered');
4691
+ this.inboundIntakeObserver = observer;
4692
+ if (this.currentInboundIntakeObserver() !== observer) {
4693
+ this.inboundIntakeObserver = undefined;
4694
+ throw new AssistantDeliveryError('runtime-conflict', 'inbound intake observer lacks current WorkItems ownership');
4695
+ }
4696
+ let live = true;
4697
+ return () => { if (live) {
4698
+ live = false;
4699
+ if (this.inboundIntakeObserver === observer)
4700
+ this.inboundIntakeObserver = undefined;
4701
+ } };
4702
+ }
4703
+ currentInboundIntakeObserver() {
4704
+ const observer = this.inboundIntakeObserver;
4705
+ if (!this.active || observer === undefined)
4706
+ return undefined;
4707
+ const peer = this.context.get('assistantWorkItems', false);
4708
+ return peer?.ownsInboundIntakeObserver?.(observer) === true ? observer : undefined;
4709
+ }
4710
+ /** Best-effort and side-effect free: any invalid input or observer failure is dropped. */
4711
+ observeInbound(observation) {
4712
+ try {
4713
+ const observer = this.currentInboundIntakeObserver();
4714
+ if (observer === undefined)
4715
+ return;
4716
+ const signal = this.inboundIntakeSignal(observation, observer.acceptsSourceText === true);
4717
+ if (signal !== undefined)
4718
+ observer.observe(signal);
4719
+ }
4720
+ catch (error) {
4721
+ this.context.logger.debug(`assistant-delivery: inbound intake observation dropped: ${String(error)}`);
4722
+ }
4723
+ }
4724
+ inboundIntakeSignal(input, withText = false) {
4725
+ const principal = input?.principal, conversation = input?.conversation;
4726
+ const text = (value) => typeof value === 'string' && value.length > 0 && value.length <= 256;
4727
+ if (!principal || !conversation || !text(input.eventId) || !text(principal.channel) || !text(principal.account)
4728
+ || !text(principal.tenant) || !text(principal.user) || !text(conversation.chat)
4729
+ || (conversation.kind !== 'dm' && conversation.kind !== 'group')
4730
+ || typeof input.addressed !== 'boolean' || typeof input.senderIsBot !== 'boolean'
4731
+ || !Number.isSafeInteger(input.occurredAt) || input.occurredAt < 0
4732
+ || (input.replyToProviderMessageId !== undefined && !text(input.replyToProviderMessageId))
4733
+ || (input.contentDigest !== undefined && !/^[0-9a-f]{64}$/u.test(input.contentDigest))
4734
+ || !/^[a-z][a-z0-9-]{0,54}$/u.test(principal.channel))
4175
4735
  return undefined;
4736
+ const key = { channel: principal.channel, account: principal.account, tenant: principal.tenant, user: principal.user };
4737
+ // Only the owner role is a trusted intake sender. A linked peer bot is
4738
+ // authorized to converse, but its messages must stay `inferred`/filtered
4739
+ // rather than entering the owner work-item allowlist.
4740
+ const principalRecord = this.deliveryStore.getPrincipal(key);
4741
+ const senderAllowlisted = principalRecord?.status === 'active' && principalRecord.role === 'owner';
4742
+ // A follow-up counts only from an allowlisted sender replying, in the same
4743
+ // conversation, to a delivered WorkItems report.
4744
+ let threadFollowUp = false;
4745
+ if (senderAllowlisted && input.replyToProviderMessageId !== undefined) {
4746
+ const report = this.deliveryStore.workItemReplies.replyReport(key.channel, key.account, input.replyToProviderMessageId);
4747
+ const target = report?.outbox.intent.target.conversation;
4748
+ threadFollowUp = report !== undefined && ['accepted', 'delivered', 'read'].includes(report.outbox.status)
4749
+ && target?.kind === conversation.kind && target.chat === conversation.chat;
4176
4750
  }
4177
- const metadata = record.intent.metadata;
4178
- const nativeKeys = Object.keys(metadata ?? {}).filter(key => key.startsWith('dsh.native-notice'));
4179
- if (nativeKeys.length > 0) {
4180
- // Native adapter sends receive only an intent, so never bind a fabricated
4181
- // outbox id into the goal-outcome sidecar lookup. A persisted record still
4182
- // rejects any sidecar collision before native authority is considered.
4183
- if (containsReservedLearningMetadata(metadata))
4184
- return { outcome: 'not-sent', failureCode: 'native-notice-invalid', retryable: false };
4185
- if (typeof record.id === 'string') {
4186
- try {
4187
- if (this.deliveryStore.getGoalOutcomeTarget(record.id) !== undefined)
4188
- return { outcome: 'not-sent', failureCode: 'native-notice-invalid', retryable: false };
4189
- }
4190
- catch {
4191
- return { outcome: 'not-sent', failureCode: 'goal-outcome-target-invalid', retryable: false };
4192
- }
4193
- }
4194
- return this.#nativeNoticeGuard(record.intent);
4751
+ return Object.freeze({
4752
+ source: `${key.channel}-channel`,
4753
+ sourceEventId: `${key.account}/${input.eventId}`,
4754
+ kind: 'message',
4755
+ senderAllowlisted,
4756
+ addressed: input.addressed,
4757
+ threadFollowUp,
4758
+ senderIsBot: input.senderIsBot,
4759
+ groupMessage: conversation.kind === 'group',
4760
+ ...(input.contentDigest === undefined ? {} : { contentDigest: input.contentDigest }),
4761
+ // Only an opted-in observer sees the text, and only when it is the digested text.
4762
+ ...(withText && input.contentDigest !== undefined && typeof input.text === 'string' && input.text.length <= MAX_INBOUND_INTAKE_TEXT
4763
+ && createHash('sha256').update(input.text, 'utf8').digest('hex') === input.contentDigest ? { sourceText: input.text } : {}),
4764
+ occurredAt: input.occurredAt,
4765
+ });
4766
+ }
4767
+ notifyWorkItemReplyPending() {
4768
+ try {
4769
+ this.currentWorkItemReplyConsumer()?.onPending();
4195
4770
  }
4196
- let goalTarget;
4771
+ catch (error) {
4772
+ this.context.logger.warn(`assistant-delivery: work item reply wake failed: ${String(error)}`);
4773
+ }
4774
+ }
4775
+ notifyWorkItemReplyCancellation(childIds) {
4776
+ if (childIds.length === 0)
4777
+ return;
4197
4778
  try {
4198
- goalTarget = this.deliveryStore.getGoalOutcomeTarget(record.id);
4779
+ this.currentWorkItemReplyConsumer()?.onCancelled(Object.freeze([...childIds]));
4199
4780
  }
4200
- catch {
4201
- return { outcome: 'not-sent', failureCode: 'goal-outcome-target-invalid', retryable: false };
4781
+ catch (error) {
4782
+ this.context.logger.warn(`assistant-delivery: work item reply cancellation failed: ${String(error)}`);
4202
4783
  }
4203
- if (goalTarget !== undefined || metadata?.['dsh.learning.kind'] === 'goal-outcome') {
4204
- if (goalTarget === undefined) {
4205
- return { outcome: 'not-sent', failureCode: 'goal-outcome-target-missing', retryable: false };
4784
+ }
4785
+ inspectWorkItemReport(input, historical = false) {
4786
+ const registration = this.currentWorkItemReplyConsumer();
4787
+ const binding = this.deliveryStore.getBinding(input.bindingId);
4788
+ const owner = binding === undefined ? undefined : this.ownerLineageForBinding(binding);
4789
+ if (!registration || input.sourceId !== registration.sourceId || !binding || (!historical && binding.status !== 'active') || !owner)
4790
+ throw new Error('work item report producer or owner unavailable');
4791
+ const target = registration.inspectReport({ itemId: input.itemId, reportRevision: input.reportRevision,
4792
+ bindingId: binding.id, idempotencyKey: input.idempotencyKey, text: input.text, format: input.format ?? 'plain' });
4793
+ if (!target || this.currentWorkItemReplyConsumer() !== registration || target.protocol !== 'assistant-delivery/work-item-report/v1'
4794
+ || target.itemId !== input.itemId || target.reportRevision !== input.reportRevision || !Number.isSafeInteger(target.reportRevision) || target.reportRevision < 1
4795
+ || typeof target.streamId !== 'string' || !target.streamId || !/^[a-f0-9]{64}$/u.test(target.reportDigest)
4796
+ || target.sourceId !== registration.sourceId || target.scope.workspace !== binding.workspace || target.scope.preset !== binding.agentPreset
4797
+ || workItemReplyCanonical(target.owner) !== workItemReplyCanonical(owner))
4798
+ throw new Error('work item report target is not eligible');
4799
+ return structuredClone(target);
4800
+ }
4801
+ enqueueWorkItemReport(input) {
4802
+ this.assertActive();
4803
+ if (containsReservedLearningMetadata(input.metadata) || Object.keys(input.metadata ?? {}).some(key => key.startsWith('dsh.native-notice') || key.startsWith(linkedReportMetadataPrefix))
4804
+ || (typeof input.idempotencyKey === 'string' && input.idempotencyKey.startsWith(linkedReportKeyPrefix))) {
4805
+ throw new AssistantDeliveryError('runtime-conflict', 'reserved metadata requires its typed delivery interface');
4806
+ }
4807
+ const target = this.inspectWorkItemReport(input);
4808
+ if (input.workspace !== target.scope.workspace)
4809
+ throw new Error('work item report scope changed');
4810
+ return this.enqueueBackgroundAuthorized(input, target, { lane: 'work-item-report', lanePurpose: { purpose: 'result', workItemId: input.itemId } });
4811
+ }
4812
+ /**
4813
+ * Host-only cross-channel route for one background work-item report.
4814
+ *
4815
+ * The origin binding always wins (default prefer): while it is active with an active
4816
+ * principal this returns `{ route: 'origin' }` and enqueues nothing, so the
4817
+ * caller keeps its existing origin path unchanged. Only when the origin is
4818
+ * unavailable does the Host walk the owner's explicit `linkedFallback` list
4819
+ * (Delivery principal ids, in order) and pick the first alias that is still
4820
+ * an active `linked` row of the CURRENT owner record, with exactly one active
4821
+ * binding in the origin workspace/preset, and that existing Policy already
4822
+ * allows to receive this source's sends. The report is queued on the plain
4823
+ * background lane, never the typed report lane, so a reply on the fallback
4824
+ * channel can never continue or give feedback on the item. The queued row
4825
+ * binds the owner and alias record versions plus the alias binding
4826
+ * version/generation; the send-time guard drops it after any revoke or
4827
+ * relink. A replay returns the first committed destination.
4828
+ *
4829
+ * Opt-in `prefer: 'recent'` instead ranks eligible aliases by the owner's
4830
+ * newest inbound message (`inbox_messages.received_at`); an available origin
4831
+ * still wins unless an eligible alias received strictly newer inbound, and
4832
+ * also when that alias is refused before its linked row commits.
4833
+ *
4834
+ * Opt-in `ownerReportRouting.orderAcrossLanes` (Delivery config) keeps one
4835
+ * item's reports in revision order across channels: when an earlier report
4836
+ * of the same item is still unsettled, the new one sticks to that route
4837
+ * (same origin, or same alias while it is still eligible and owner-listed)
4838
+ * so the existing per-lane claim order sends it after. A sticky origin also
4839
+ * overrides `prefer: 'recent'`. An unusable sticky route falls back to the
4840
+ * rules above, so ordering never blocks delivery.
4841
+ */
4842
+ routeWorkItemReport(input) {
4843
+ this.assertActive();
4844
+ const sourceId = canonicalBackgroundSourceId(input.sourceId);
4845
+ if (!Array.isArray(input.linkedFallback) || input.linkedFallback.length > maxLinkedReportFallback
4846
+ || input.linkedFallback.some(id => typeof id !== 'string' || id.length === 0 || id.length > 256)
4847
+ || typeof input.idempotencyKey !== 'string' || input.idempotencyKey.length === 0
4848
+ || typeof input.itemId !== 'string' || input.itemId.length === 0
4849
+ || !Number.isSafeInteger(input.reportRevision) || input.reportRevision < 1
4850
+ || (input.prefer !== undefined && input.prefer !== 'origin' && input.prefer !== 'recent')) {
4851
+ throw new AssistantDeliveryError('runtime-conflict', 'linked report route input is invalid');
4852
+ }
4853
+ const linkedKey = `${linkedReportKeyPrefix}${createHash('sha256').update(input.idempotencyKey).digest('hex')}`;
4854
+ // Never send one report twice: a committed destination is final.
4855
+ if (this.deliveryStore.getOutboxByIdempotencyKey(input.idempotencyKey) !== undefined)
4856
+ return Object.freeze({ route: 'origin' });
4857
+ const replay = this.deliveryStore.getOutboxByIdempotencyKey(linkedKey);
4858
+ if (replay !== undefined) {
4859
+ const metadata = replay.intent.metadata;
4860
+ if (metadata?.[linkedReportMetadataPrefix] !== 'v1' || metadata[`${linkedReportMetadataPrefix}.sourceId`] !== sourceId
4861
+ || metadata[`${linkedReportMetadataPrefix}.originBindingId`] !== input.originBindingId
4862
+ || metadata[`${linkedReportMetadataPrefix}.itemId`] !== input.itemId
4863
+ || metadata[`${linkedReportMetadataPrefix}.reportRevision`] !== String(input.reportRevision)) {
4864
+ throw new AssistantDeliveryError('runtime-conflict', 'linked report idempotency key was reused for a different report');
4206
4865
  }
4207
- const locator = goalTarget.locator;
4208
- const proof = goalTarget.proof;
4209
- const binding = this.deliveryStore.getBinding(record.intent.bindingId);
4210
- const owner = binding === undefined ? undefined : this.deliveryStore.getPrincipal(binding.principal);
4866
+ return Object.freeze({ route: 'linked', outbox: replay });
4867
+ }
4868
+ const origin = this.deliveryStore.getBinding(input.originBindingId);
4869
+ if (origin === undefined || origin.workspace !== input.workspace) {
4870
+ throw new AssistantDeliveryError('missing-binding', 'report origin binding does not exist');
4871
+ }
4872
+ const recent = input.prefer === 'recent';
4873
+ // Replays returned above, so a sticky row is always another report of this item.
4874
+ const sticky = this.config.ownerReportRouting?.orderAcrossLanes === true
4875
+ ? this.deliveryStore.findUnsettledWorkItemReport({ sourceIds: [sourceId, input.sourceId], workspace: input.workspace,
4876
+ originBindingId: origin.id, itemId: input.itemId, beforeRevision: input.reportRevision })
4877
+ : undefined;
4878
+ const stickyLinked = sticky?.route === 'linked';
4879
+ // A sticky origin only orders a lane the caller can still address: after a
4880
+ // `/new` rotation the frozen origin binding is revoked, so forcing it would
4881
+ // make the caller's origin enqueue fail where `prefer: 'recent'` could have
4882
+ // delivered on an alias. Fall through to normal routing in that case.
4883
+ const originDirect = origin.status === 'active' && this.hasActivePrincipal(origin);
4884
+ if (originDirect && !recent && !stickyLinked)
4885
+ return Object.freeze({ route: 'origin' });
4886
+ // A `/new` rotation revokes the origin and inserts a successor in the same
4887
+ // conversation for the same principal. That origin is still available: keep
4888
+ // the origin path (and its existing failure semantics) rather than reroute.
4889
+ const successor = origin.status === 'active' ? undefined : this.deliveryStore.getActiveBinding(origin.conversation);
4890
+ const originAvailable = originDirect || (successor !== undefined && successor.id !== origin.id
4891
+ && successor.workspace === origin.workspace && successor.agentPreset === origin.agentPreset
4892
+ && JSON.stringify(successor.principal) === JSON.stringify(origin.principal) && this.hasActivePrincipal(successor));
4893
+ if (originAvailable && ((!recent && !stickyLinked) || (sticky?.route === 'origin' && originDirect)))
4894
+ return Object.freeze({ route: 'origin' });
4895
+ const owner = this.deliveryStore.getPrincipal(origin.principal);
4896
+ if (owner?.status !== 'active' || owner.role !== 'owner' || JSON.stringify(owner.principal) !== JSON.stringify(origin.principal)) {
4897
+ if (originAvailable)
4898
+ return Object.freeze({ route: 'origin' });
4899
+ throw new AssistantDeliveryError('missing-binding', 'report origin owner is no longer active');
4900
+ }
4901
+ const ownerId = externalPrincipalId(owner.principal);
4902
+ const active = this.deliveryStore.listActiveBindings();
4903
+ const eligible = (aliasId) => {
4904
+ if (aliasId === ownerId)
4905
+ return undefined;
4906
+ const matches = active.filter(binding => externalPrincipalId(binding.principal) === aliasId
4907
+ && binding.workspace === origin.workspace && binding.agentPreset === origin.agentPreset);
4908
+ // Ambiguous alias channels are skipped rather than guessed.
4909
+ if (matches.length !== 1)
4910
+ return undefined;
4911
+ const binding = matches[0];
4912
+ const alias = this.deliveryStore.getPrincipal(binding.principal);
4913
+ if (binding.id === origin.id || alias?.status !== 'active' || alias.role !== 'linked' || alias.linkedToId !== owner.id
4914
+ || alias.id === owner.id || JSON.stringify(alias.principal) !== JSON.stringify(binding.principal))
4915
+ return undefined;
4916
+ if (!this.linkedReportSendAllowed(sourceId, binding))
4917
+ return undefined;
4918
+ return { binding, alias };
4919
+ };
4920
+ let picked;
4921
+ if (stickyLinked) {
4922
+ // Same alias binding only: a revoked, relinked, unlisted or ambiguous alias is not sticky.
4923
+ const binding = this.deliveryStore.getBinding(sticky.bindingId);
4924
+ const aliasId = binding === undefined ? undefined : externalPrincipalId(binding.principal);
4925
+ const found = aliasId === undefined || !input.linkedFallback.includes(aliasId) ? undefined : eligible(aliasId);
4926
+ // A kept alias queues behind the earlier report through the per-lane claim order.
4927
+ if (found?.binding.id === sticky.bindingId)
4928
+ picked = found;
4929
+ else if (originAvailable && !recent)
4930
+ return Object.freeze({ route: 'origin' });
4931
+ }
4932
+ const stickyPicked = picked !== undefined;
4933
+ if (!stickyPicked && recent) {
4934
+ // Opt-in: rank eligible aliases (one Policy evaluate each) by the owner's
4935
+ // newest inbound message (stable sort keeps list order on ties). An
4936
+ // available origin wins unless an eligible alias received strictly newer inbound.
4937
+ const candidates = input.linkedFallback.flatMap(aliasId => {
4938
+ const found = eligible(aliasId);
4939
+ return found === undefined ? [] : [found];
4940
+ });
4941
+ const latest = this.deliveryStore.latestInboundAtByBinding([origin.id, ...candidates.map(candidate => candidate.binding.id)]);
4942
+ const at = (bindingId) => latest.get(bindingId) ?? Number.NEGATIVE_INFINITY;
4943
+ candidates.sort((left, right) => at(left.binding.id) === at(right.binding.id) ? 0 : at(left.binding.id) < at(right.binding.id) ? 1 : -1);
4944
+ picked = candidates[0];
4945
+ if (originAvailable && (picked === undefined || !(at(picked.binding.id) > at(origin.id))))
4946
+ return Object.freeze({ route: 'origin' });
4947
+ }
4948
+ else if (!stickyPicked) {
4949
+ for (const aliasId of input.linkedFallback) {
4950
+ picked = eligible(aliasId);
4951
+ if (picked !== undefined)
4952
+ break;
4953
+ }
4954
+ }
4955
+ if (picked !== undefined) {
4956
+ const { binding, alias } = picked;
4957
+ let outbox;
4211
4958
  try {
4212
- if (proof.receipt.completedAt > Date.now() || proof.receipt.validUntil <= Date.now()) {
4213
- throw new Error('goal outcome receipt expired');
4959
+ outbox = this.enqueueBackgroundAuthorized({ sourceId, workspace: binding.workspace, bindingId: binding.id,
4960
+ idempotencyKey: linkedKey, text: input.text, format: input.format ?? 'plain', metadata: {
4961
+ [linkedReportMetadataPrefix]: 'v1',
4962
+ [`${linkedReportMetadataPrefix}.sourceId`]: sourceId,
4963
+ [`${linkedReportMetadataPrefix}.itemId`]: input.itemId,
4964
+ [`${linkedReportMetadataPrefix}.reportRevision`]: String(input.reportRevision),
4965
+ [`${linkedReportMetadataPrefix}.originBindingId`]: origin.id,
4966
+ [`${linkedReportMetadataPrefix}.ownerRecordId`]: owner.id,
4967
+ [`${linkedReportMetadataPrefix}.ownerVersion`]: String(owner.version),
4968
+ [`${linkedReportMetadataPrefix}.aliasRecordId`]: alias.id,
4969
+ [`${linkedReportMetadataPrefix}.aliasVersion`]: String(alias.version),
4970
+ [`${linkedReportMetadataPrefix}.bindingVersion`]: String(binding.version),
4971
+ [`${linkedReportMetadataPrefix}.bindingGeneration`]: String(binding.generation),
4972
+ } }, undefined, { lane: 'background', lanePurpose: { purpose: 'result', workItemId: input.itemId } });
4973
+ }
4974
+ catch (error) {
4975
+ // `prefer: 'recent'` or a sticky alias with an available origin: a refusal raised before
4976
+ // the linked row commits (Policy authorize deny, attention enforce,
4977
+ // binding change) keeps the origin path instead of losing the report.
4978
+ // A committed linked row is final and is never paired with an origin send.
4979
+ if ((recent || stickyPicked) && originAvailable && error instanceof AssistantDeliveryError
4980
+ && this.deliveryStore.getOutboxByIdempotencyKey(linkedKey) === undefined)
4981
+ return Object.freeze({ route: 'origin' });
4982
+ throw error;
4983
+ }
4984
+ return Object.freeze({ route: 'linked', outbox });
4985
+ }
4986
+ throw new AssistantDeliveryError('missing-binding', 'report origin is unavailable and no linked fallback is eligible');
4987
+ }
4988
+ linkedReportSendAllowed(sourceId, binding) {
4989
+ return this.policy.evaluate({ subject: { kind: 'background', id: sourceId, workspace: binding.workspace,
4990
+ principal: externalPrincipalId(binding.principal) }, action: 'send', resource: { kind: 'message', id: binding.id },
4991
+ context: { initiator: 'background' } }).effect === 'allow';
4992
+ }
4993
+ /** Send-time re-check of a linked report route; any revoke, relink or Policy change drops it. */
4994
+ linkedReportGuard(record) {
4995
+ const metadata = record.intent.metadata;
4996
+ const field = (name) => metadata?.[`${linkedReportMetadataPrefix}.${name}`];
4997
+ try {
4998
+ if (containsReservedLearningMetadata(metadata) || metadata?.[linkedReportMetadataPrefix] !== 'v1'
4999
+ || !record.intent.idempotencyKey.startsWith(linkedReportKeyPrefix)
5000
+ || (typeof record.id === 'string' && this.deliveryStore.getGoalOutcomeTarget(record.id) !== undefined))
5001
+ throw new Error('linked report is invalid');
5002
+ const binding = this.deliveryStore.getBinding(record.intent.bindingId);
5003
+ const alias = binding === undefined ? undefined : this.deliveryStore.getPrincipal(binding.principal);
5004
+ const origin = this.deliveryStore.getBinding(field('originBindingId') ?? '');
5005
+ const owner = origin === undefined ? undefined : this.deliveryStore.getPrincipal(origin.principal);
5006
+ if (binding?.status !== 'active' || String(binding.version) !== field('bindingVersion') || String(binding.generation) !== field('bindingGeneration')
5007
+ || origin === undefined || binding.workspace !== origin.workspace || binding.agentPreset !== origin.agentPreset
5008
+ || owner?.status !== 'active' || owner.role !== 'owner' || owner.id !== field('ownerRecordId') || String(owner.version) !== field('ownerVersion')
5009
+ || alias?.status !== 'active' || alias.role !== 'linked' || alias.id !== field('aliasRecordId') || String(alias.version) !== field('aliasVersion')
5010
+ || alias.linkedToId !== owner.id || !this.linkedReportSendAllowed(canonicalBackgroundSourceId(field('sourceId') ?? ''), binding)) {
5011
+ throw new Error('linked report route changed');
5012
+ }
5013
+ }
5014
+ catch {
5015
+ return { outcome: 'not-sent', failureCode: 'linked-report-route-revoked', retryable: false };
5016
+ }
5017
+ return undefined;
5018
+ }
5019
+ workItemReplyPermission(binding, registration) {
5020
+ return this.policy.evaluate({ subject: { kind: 'external', id: externalPrincipalId(binding.principal), workspace: binding.workspace, principal: externalPrincipalId(binding.principal) },
5021
+ action: 'continue-work-items-from-reply', resource: { kind: 'automation', id: registration.continuationAutomationId },
5022
+ context: { initiator: 'external' } }).effect === 'allow';
5023
+ }
5024
+ /**
5025
+ * Validate the continuation lane of one reply to an exact report without
5026
+ * writing it. Mixed-intent admission reuses this report inside its combined
5027
+ * transaction; the ordinary path admits it through `admitWorkItemReply`.
5028
+ */
5029
+ prepareWorkItemReply(inbox, binding) {
5030
+ const registration = this.currentWorkItemReplyConsumer();
5031
+ const envelope = inbox.envelope;
5032
+ if (!registration || registration.continuationEnabled === false || envelope.kind !== 'text' || envelope.text.trim().startsWith('/') || !envelope.text
5033
+ || (envelope.attachments?.length ?? 0) > 0 || !envelope.metadata?.replyToProviderMessageId)
5034
+ return undefined;
5035
+ const report = this.deliveryStore.workItemReplies.replyReport(envelope.channel, envelope.account, envelope.metadata.replyToProviderMessageId);
5036
+ const outbox = report?.outbox;
5037
+ if (!report || !outbox || !['accepted', 'delivered', 'read'].includes(outbox.status) || workItemReplyCanonical(outbox.intent.target.principal) !== workItemReplyCanonical(binding.principal)
5038
+ || workItemReplyCanonical(outbox.intent.target.conversation) !== workItemReplyCanonical(envelope.conversation))
5039
+ return undefined;
5040
+ try {
5041
+ const target = this.inspectWorkItemReport({ sourceId: registration.sourceId, itemId: report.target.itemId,
5042
+ reportRevision: report.target.reportRevision, bindingId: outbox.intent.bindingId, idempotencyKey: outbox.intent.idempotencyKey,
5043
+ text: outbox.intent.text, format: outbox.intent.format === 'markdown' ? 'markdown' : 'plain' }, true);
5044
+ if (workItemReplyCanonical(target) !== workItemReplyCanonical(report.target))
5045
+ return undefined;
5046
+ if (registration.canContinueReport?.({ itemId: target.itemId, reportRevision: target.reportRevision, bindingId: outbox.intent.bindingId,
5047
+ idempotencyKey: outbox.intent.idempotencyKey, text: outbox.intent.text, format: outbox.intent.format === 'markdown' ? 'markdown' : 'plain' }) === false)
5048
+ return undefined;
5049
+ if (this.currentWorkItemReplyConsumer() !== registration || !this.workItemReplyPermission(binding, registration))
5050
+ return undefined;
5051
+ }
5052
+ catch {
5053
+ return undefined;
5054
+ }
5055
+ return report;
5056
+ }
5057
+ admitWorkItemReply(inbox, binding) {
5058
+ const report = this.prepareWorkItemReply(inbox, binding);
5059
+ if (report === undefined)
5060
+ return false;
5061
+ // From here a failed durable handoff is an error, never ordinary fallback.
5062
+ this.deliveryStore.workItemReplies.admit(inbox.id, binding, report);
5063
+ return true;
5064
+ }
5065
+ workItemReplyFence(source) {
5066
+ const registration = this.currentWorkItemReplyConsumer();
5067
+ const fresh = this.deliveryStore.workItemReplies.read({ id: source.id, expectedSourceDigest: source.sourceDigest });
5068
+ if (workItemReplyCanonical(fresh) !== workItemReplyCanonical(source))
5069
+ throw new Error('work item reply source state changed');
5070
+ const binding = this.deliveryStore.getBinding(source.binding.id);
5071
+ const inbox = this.deliveryStore.getInbox(source.id);
5072
+ const report = this.deliveryStore.workItemReplies.report(source.target.outboxId);
5073
+ if (!registration || registration.continuationEnabled === false || !binding || binding.status !== 'active' || binding.version !== source.binding.version || binding.generation !== source.binding.generation
5074
+ || !inbox || inbox.status !== 'processed' || inbox.bindingId !== binding.id || !report
5075
+ || inbox.envelope.text !== source.inputText || workItemReplyCanonical(inbox.admissionCursor) !== workItemReplyCanonical(source.admissionCursor)
5076
+ || workItemReplyCanonical(this.ownerLineageForBinding(binding)) !== workItemReplyCanonical(source.owner)
5077
+ || externalPrincipalId(binding.principal) !== source.principalId || binding.workspace !== source.scope.workspace || binding.agentPreset !== source.scope.preset
5078
+ || !['accepted', 'delivered', 'read'].includes(report.outbox.status) || report.outbox.providerMessageId !== source.target.providerMessageId
5079
+ || this.deliveryStore.workItemReplies.replyReport(binding.conversation.channel, binding.conversation.account, source.target.providerMessageId)?.outbox.id !== source.target.outboxId
5080
+ || workItemReplyCanonical(report.outbox.intent.target.conversation) !== workItemReplyCanonical(inbox.envelope.conversation)
5081
+ || !this.workItemReplyPermission(binding, registration))
5082
+ throw new Error('work item reply owner, producer or policy unavailable');
5083
+ return registration;
5084
+ }
5085
+ validateWorkItemReply(source) {
5086
+ const registration = this.workItemReplyFence(source);
5087
+ const report = this.deliveryStore.workItemReplies.report(source.target.outboxId);
5088
+ const target = this.inspectWorkItemReport({ sourceId: registration.sourceId, itemId: source.target.itemId,
5089
+ reportRevision: source.target.reportRevision, bindingId: report.outbox.intent.bindingId, idempotencyKey: report.outbox.intent.idempotencyKey,
5090
+ text: report.outbox.intent.text, format: report.outbox.intent.format === 'markdown' ? 'markdown' : 'plain' }, true);
5091
+ if (workItemReplyCanonical(target) !== workItemReplyCanonical(report.target))
5092
+ throw new Error('work item reply report changed');
5093
+ if (registration.canContinueReport?.({ itemId: target.itemId, reportRevision: target.reportRevision, bindingId: report.outbox.intent.bindingId,
5094
+ idempotencyKey: report.outbox.intent.idempotencyKey, text: report.outbox.intent.text, format: report.outbox.intent.format === 'markdown' ? 'markdown' : 'plain' }) === false)
5095
+ throw new Error('report continuation authority expired');
5096
+ if (this.workItemReplyFence(source) !== registration)
5097
+ throw new Error('work item reply authority changed');
5098
+ }
5099
+ listPendingWorkItemReplies(input) {
5100
+ this.assertActive();
5101
+ if (!this.currentWorkItemReplyConsumer())
5102
+ throw new Error('work item reply producer unavailable');
5103
+ if (!Number.isSafeInteger(input.limit) || input.limit < 1 || input.limit > 100 || (input.afterSequence !== undefined && (!Number.isSafeInteger(input.afterSequence) || input.afterSequence < 0)))
5104
+ throw new Error('invalid work item reply scan');
5105
+ const raw = this.deliveryStore.workItemReplies.pending(input);
5106
+ const hasMore = raw.length > input.limit;
5107
+ const page = raw.slice(0, input.limit);
5108
+ const eligible = page.filter(source => {
5109
+ try {
5110
+ this.validateWorkItemReply(source);
5111
+ return true;
5112
+ }
5113
+ catch {
5114
+ return false;
5115
+ }
5116
+ });
5117
+ return Object.freeze({ items: Object.freeze(eligible), hasMore, nextAfterSequence: hasMore ? page.at(-1).admissionCursor.sequence : null });
5118
+ }
5119
+ readWorkItemReply(input) {
5120
+ this.assertActive();
5121
+ const source = this.deliveryStore.workItemReplies.read(input);
5122
+ this.validateWorkItemReply(source);
5123
+ return source;
5124
+ }
5125
+ selectWorkItemReplyParent(input) {
5126
+ this.assertActive();
5127
+ return this.deliveryStore.workItemReplies.select(input, source => this.validateWorkItemReply(source));
5128
+ }
5129
+ acknowledgeWorkItemReply(input) {
5130
+ this.assertActive();
5131
+ const source = this.deliveryStore.workItemReplies.acknowledge(input, source => {
5132
+ this.validateWorkItemReply(source);
5133
+ const registration = this.currentWorkItemReplyConsumer();
5134
+ if (!source.executionParentItemId || !registration || registration.inspectChild({ replyId: source.id, sourceDigest: source.sourceDigest,
5135
+ parentItemId: source.executionParentItemId, childItemId: input.childItemId, childPlanDigest: input.childPlanDigest }) !== true
5136
+ || this.workItemReplyFence(source) !== registration)
5137
+ throw new Error('work item reply child is not durably verified');
5138
+ });
5139
+ this.notifyWorkItemReplyPending();
5140
+ return source;
5141
+ }
5142
+ enqueueBackground(input) {
5143
+ this.assertActive();
5144
+ if (containsReservedLearningMetadata(input.metadata) || Object.keys(input.metadata ?? {}).some(key => key.startsWith('dsh.native-notice') || key.startsWith(linkedReportMetadataPrefix))
5145
+ || (typeof input.idempotencyKey === 'string' && input.idempotencyKey.startsWith(linkedReportKeyPrefix))) {
5146
+ throw new AssistantDeliveryError('runtime-conflict', 'reserved metadata requires its typed delivery interface');
5147
+ }
5148
+ return this.enqueueBackgroundAuthorized(input, undefined, { lane: 'background', envelope: input.attention });
5149
+ }
5150
+ /**
5151
+ * Record the WP19 stage-1 verdict for one queued Outbox record. Audit only:
5152
+ * the record is returned unchanged and any audit failure is swallowed.
5153
+ */
5154
+ auditAttention(lane, sourceId, audit, record) {
5155
+ try {
5156
+ this.deliveryStore.attentionAudit.record({ outboxId: record.id, bindingId: record.intent.bindingId, lane, sourceId }, ledger => assessAttention({ text: record.intent.attachment?.text ?? record.intent.text, ...(audit.envelope === undefined ? {} : { envelope: audit.envelope }),
5157
+ ...(audit.lanePurpose === undefined ? {} : { lanePurpose: audit.lanePurpose }), ledger }));
5158
+ }
5159
+ catch {
5160
+ this.attentionAuditWriteFailures += 1;
5161
+ }
5162
+ return record;
5163
+ }
5164
+ /**
5165
+ * WP19 stage 2b, only when `attentionAttachOverCap` is on: convert an
5166
+ * over-cap notice to a short body plus one text attachment (never split),
5167
+ * and reject an inline notice that, with recent inline notices from the same
5168
+ * source, binding and lane, exceeds the cap of its purpose. Deterministic for
5169
+ * one (input, audited history, clock). Off: the input is returned unchanged,
5170
+ * except that a replay of an already converted key keeps its stored form.
5171
+ * WP19 stage 3, only when `attentionEnforce` is on: a new notice with a
5172
+ * missing or failed purpose, or over cap without the attachment path, is
5173
+ * rejected with `AttentionRejectedError` and nothing is queued. Replays
5174
+ * return above before any check, so an existing key stays idempotent.
5175
+ */
5176
+ prepareAttentionDelivery(input) {
5177
+ const unchanged = { text: input.text, format: input.format };
5178
+ let winner;
5179
+ try {
5180
+ winner = this.deliveryStore.getOutboxByIdempotencyKey(input.idempotencyKey);
5181
+ }
5182
+ catch {
5183
+ winner = undefined;
5184
+ }
5185
+ // A replay keeps the stored form even after the flag is turned off, so a converted row
5186
+ // stays idempotent; the store still rejects a changed intent under the same key.
5187
+ if (winner !== undefined) {
5188
+ const stored = winner.intent.attachment;
5189
+ return stored !== undefined && stored.text === input.text && stored.mediaType === (input.format === 'markdown' ? 'text/markdown' : 'text/plain')
5190
+ ? { text: winner.intent.text, format: 'plain', attachment: stored } : unchanged;
5191
+ }
5192
+ const attach = this.config.attentionAttachOverCap === true;
5193
+ const enforce = this.config.attentionEnforce === true;
5194
+ if (!attach && !enforce)
5195
+ return unchanged;
5196
+ const binding = typeof input.binding === 'function' ? input.binding() : input.binding;
5197
+ // No active binding: the store rejects the enqueue with its usual error.
5198
+ if (binding === undefined)
5199
+ return unchanged;
5200
+ const assessment = assessAttention({ text: input.text, ...(input.envelope === undefined ? {} : { envelope: input.envelope }),
5201
+ ...(input.lanePurpose === undefined ? {} : { lanePurpose: input.lanePurpose }),
5202
+ ledger: this.deliveryStore.attentionAudit.ledger(binding.id) });
5203
+ const purpose = assessment.purpose;
5204
+ // Stage 3: every rejection is recorded before it is thrown; without enforcement the stage 2b errors stay as they were.
5205
+ const reject = (reasons, message) => {
5206
+ if (!enforce)
5207
+ throw new AssistantDeliveryError('policy-denied', message);
5208
+ throw new AttentionRejectedError(this.attentionRejections.record({ bindingId: binding.id, lane: input.lane, sourceId: input.sourceId,
5209
+ idempotencyKey: input.idempotencyKey, assessment, reasons }), message);
5210
+ };
5211
+ if (enforce) {
5212
+ const reasons = attentionEnforcementReasons(assessment, attach);
5213
+ if (reasons.length > 0)
5214
+ reject(reasons);
5215
+ }
5216
+ let delivered = unchanged;
5217
+ if (attach && attentionOverCap(input.text, purpose)) {
5218
+ const adapter = this.registry.get(binding.conversation.channel, binding.conversation.account);
5219
+ if (adapter === undefined) {
5220
+ throw new AssistantDeliveryError('runtime-unavailable', 'over-cap notice needs its channel adapter to attach the full text');
5221
+ }
5222
+ if (adapter.capabilities.textAttachments !== true) {
5223
+ reject([...assessment.reasons.filter(reason => reason === 'over-word-cap' || reason === 'over-grapheme-cap'), 'attachment-unsupported'], 'over-cap notice cannot be attached: the channel does not accept text attachments');
5224
+ }
5225
+ delivered = convertOverCapAttention({ text: input.text, format: input.format,
5226
+ digest: text => createHash('sha256').update(text).digest('hex') });
5227
+ }
5228
+ // Split evasion is a stage 2b check; enforcement alone does not add it.
5229
+ if (!attach)
5230
+ return delivered;
5231
+ const split = assessAttentionSplit({ text: delivered.text, purpose, priorInlineTexts: this.deliveryStore.attentionAudit.recentInlineTexts({
5232
+ bindingId: binding.id, lane: input.lane, sourceId: input.sourceId, windowMs: attentionSplitWindowMs
5233
+ }) });
5234
+ if (split.split) {
5235
+ reject(['split-evasion'], `attention split evasion: ${split.priorNotices} recent notice(s) from this source plus this one exceed the ${purpose} cap; send one notice`);
5236
+ }
5237
+ return delivered;
5238
+ }
5239
+ /**
5240
+ * Read-only WP19 stage-1 audit page in append order; never exposes message text.
5241
+ * `processLocalWriteFailures` counts failed audit writes since this Host started;
5242
+ * `unauditedOutbox` is derived from durable rows on every call (see
5243
+ * `AttentionAuditStore.unauditedOutboxCount` for which lanes it can see).
5244
+ */
5245
+ inspectAttentionAudit(input = {}) {
5246
+ this.assertActive();
5247
+ const afterSequence = input.afterSequence ?? 0;
5248
+ const limit = input.limit ?? 100;
5249
+ if (!Number.isSafeInteger(afterSequence) || afterSequence < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
5250
+ throw new AssistantDeliveryError('runtime-conflict', 'invalid attention audit scan');
5251
+ }
5252
+ const page = this.deliveryStore.attentionAudit.list(afterSequence, limit);
5253
+ return Object.freeze({ entries: page.entries, hasMore: page.hasMore,
5254
+ nextAfterSequence: page.hasMore ? page.entries.at(-1).sequence : null,
5255
+ processLocalWriteFailures: this.attentionAuditWriteFailures, unauditedOutbox: this.deliveryStore.attentionAudit.unauditedOutboxCount() });
5256
+ }
5257
+ /**
5258
+ * WP22 read adapter for Evaluation: per-notification durable facts (transport,
5259
+ * owner reply, current owner judgement) keyed by audited Outbox id. Read-only,
5260
+ * text-free, and classification-free. Pass `outboxIds` (max 500) to refresh
5261
+ * known notifications, or `afterSequence`/`limit` to page new audit rows.
5262
+ */
5263
+ inspectNotificationOutcomeFacts(input = {}) {
5264
+ this.assertActive();
5265
+ if (input.outboxIds !== undefined) {
5266
+ if (!Array.isArray(input.outboxIds) || input.outboxIds.length > 500 || input.afterSequence !== undefined || input.limit !== undefined
5267
+ || !input.outboxIds.every(id => typeof id === 'string' && id.length > 0 && id.length <= 256)) {
5268
+ throw new AssistantDeliveryError('runtime-conflict', 'invalid notification outcome lookup');
5269
+ }
5270
+ return Object.freeze({ protocol: NOTIFICATION_OUTCOME_FACTS_PROTOCOL,
5271
+ facts: this.deliveryStore.attentionAudit.outcomeFactsFor([...new Set(input.outboxIds)]), hasMore: false, nextAfterSequence: null });
5272
+ }
5273
+ const afterSequence = input.afterSequence ?? 0;
5274
+ const limit = input.limit ?? 100;
5275
+ if (!Number.isSafeInteger(afterSequence) || afterSequence < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
5276
+ throw new AssistantDeliveryError('runtime-conflict', 'invalid notification outcome scan');
5277
+ }
5278
+ const page = this.deliveryStore.attentionAudit.list(afterSequence, limit);
5279
+ const facts = this.deliveryStore.attentionAudit.outcomeFactsFor(page.entries.map(entry => entry.outboxId));
5280
+ return Object.freeze({ protocol: NOTIFICATION_OUTCOME_FACTS_PROTOCOL, facts, hasMore: page.hasMore,
5281
+ nextAfterSequence: page.entries.length === 0 ? null : page.entries.at(-1).sequence });
5282
+ }
5283
+ /**
5284
+ * Read-only WP19 stage-2c progress audit page in append order; never exposes
5285
+ * progress text. Entries are process-local (`durable: false`), bounded, and
5286
+ * reset on Host restart; `processLocalWriteFailures` counts failed records.
5287
+ */
5288
+ inspectProgressAttentionAudit(input = {}) {
5289
+ this.assertActive();
5290
+ const afterSequence = input.afterSequence ?? 0;
5291
+ const limit = input.limit ?? 100;
5292
+ if (!Number.isSafeInteger(afterSequence) || afterSequence < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
5293
+ throw new AssistantDeliveryError('runtime-conflict', 'invalid progress attention audit scan');
5294
+ }
5295
+ return Object.freeze({ ...this.progressAttentionAudit.list(afterSequence, limit),
5296
+ processLocalWriteFailures: this.progressAttentionAuditFailures });
5297
+ }
5298
+ /**
5299
+ * Read-only WP19 stage-3 rejection page in append order; never exposes
5300
+ * message text. Entries are process-local (`durable: false`), bounded, and
5301
+ * reset on Host restart. Empty while `attentionEnforce` is off.
5302
+ */
5303
+ inspectAttentionRejections(input = {}) {
5304
+ this.assertActive();
5305
+ const afterSequence = input.afterSequence ?? 0;
5306
+ const limit = input.limit ?? 100;
5307
+ if (!Number.isSafeInteger(afterSequence) || afterSequence < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
5308
+ throw new AssistantDeliveryError('runtime-conflict', 'invalid attention rejection scan');
5309
+ }
5310
+ return this.attentionRejections.list(afterSequence, limit);
5311
+ }
5312
+ enqueueBackgroundAuthorized(input, reportTarget, audit) {
5313
+ const binding = this.deliveryStore.getBinding(input.bindingId);
5314
+ if (binding === undefined || binding.status !== 'active' || !this.hasActivePrincipal(binding)) {
5315
+ throw new AssistantDeliveryError('missing-binding', 'delivery binding does not exist or is revoked');
5316
+ }
5317
+ if (binding.workspace !== input.workspace) {
5318
+ throw new AssistantDeliveryError('missing-binding', 'delivery workspace does not match the active binding');
5319
+ }
5320
+ const prepare = () => this.prepareAttentionDelivery({ ...audit, sourceId: input.sourceId, binding,
5321
+ idempotencyKey: input.idempotencyKey, text: input.text, format: input.format ?? 'plain' });
5322
+ // Stage 3: the attention verdict is pure over the audited ledger, so an enforced
5323
+ // rejection happens before Policy writes its allow row or spends budget.
5324
+ const early = this.config.attentionEnforce === true ? prepare() : undefined;
5325
+ const decision = this.policy.authorize({ subject: { kind: 'background', id: input.sourceId,
5326
+ workspace: input.workspace, principal: externalPrincipalId(binding.principal) }, action: 'send',
5327
+ resource: { kind: 'message', id: binding.id }, context: { initiator: 'background' } }, { idempotencyKey: `message-send:${input.idempotencyKey}` });
5328
+ if (decision.effect !== 'allow')
5329
+ throw policyDenied(decision);
5330
+ if (!this.hasActivePrincipal(binding)) {
5331
+ throw new AssistantDeliveryError('missing-binding', 'delivery binding principal is no longer active');
5332
+ }
5333
+ const delivered = early ?? prepare();
5334
+ const enqueue = () => this.deliveryStore.enqueue({ idempotencyKey: input.idempotencyKey, bindingId: binding.id,
5335
+ target: { conversation: binding.conversation, principal: binding.principal }, ...delivered,
5336
+ ...(input.metadata === undefined ? {} : { metadata: input.metadata }) });
5337
+ return this.auditAttention(audit.lane, input.sourceId, audit, reportTarget === undefined ? enqueue() : this.deliveryStore.workItemReplies.enqueueReport(reportTarget, binding, enqueue));
5338
+ }
5339
+ /** `withdrawal: false` is for listing already-accepted notices; every send path keeps the default. */
5340
+ #notificationGuard(record, options = { withdrawal: true }) {
5341
+ const key = record.intent.idempotencyKey;
5342
+ if (key.startsWith('inbound:') && (key.endsWith(':maintenance') || key.endsWith(':maintenance-failed'))) {
5343
+ try {
5344
+ const failed = key.endsWith(':maintenance-failed');
5345
+ const inboxId = key.slice('inbound:'.length, -(failed ? ':maintenance-failed'.length : ':maintenance'.length));
5346
+ const request = this.restartStore?.getRequestForTask(inboxId);
5347
+ const task = this.restartStore?.getTask(inboxId);
5348
+ const inbox = this.deliveryStore.getInbox(inboxId);
5349
+ const binding = this.deliveryStore.getBinding(record.intent.bindingId);
5350
+ if (request === undefined || task === undefined || inbox === undefined || binding === undefined
5351
+ || request.status === 'cancelled' || (failed ? request.status !== 'failed' : !this.restartAuthorized(request))
5352
+ || task.binding.id !== binding.id || task.binding.version !== binding.version || task.binding.generation !== binding.generation
5353
+ || task.owner.principalRecordId !== this.ownerLineageForBinding(binding)?.principalRecordId
5354
+ || task.owner.principalVersion !== this.ownerLineageForBinding(binding)?.principalVersion
5355
+ || inbox.bindingId !== binding.id || inbox.eventId !== record.intent.replyToEventId
5356
+ || !this.isInboundAuthorized(binding, inbox.envelope)
5357
+ || this.deliveryStore.getOutboxByIdempotencyKey(`inbound:${inbox.id}:reply`) !== undefined
5358
+ || this.deliveryStore.getOutboxByIdempotencyKey(`inbound:${inbox.eventId}:reply`) !== undefined)
5359
+ throw new Error('maintenance notice is stale');
5360
+ const decision = this.policy.evaluate({ subject: { kind: 'agent', id: binding.agentPreset,
5361
+ workspace: binding.workspace, principal: externalPrincipalId(binding.principal) },
5362
+ action: 'reply', resource: { kind: 'message', id: binding.id }, context: { initiator: 'external' } });
5363
+ if (decision.effect !== 'allow')
5364
+ throw new Error('maintenance notice permission changed');
5365
+ }
5366
+ catch {
5367
+ return { outcome: 'not-sent', failureCode: 'maintenance-notice-stale', retryable: false };
5368
+ }
5369
+ return undefined;
5370
+ }
5371
+ if (key.startsWith('inbound:') && key.endsWith(':interrupted')) {
5372
+ try {
5373
+ const inboxId = key.slice('inbound:'.length, -':interrupted'.length);
5374
+ const inbox = this.deliveryStore.getInbox(inboxId);
5375
+ const binding = this.deliveryStore.getBinding(record.intent.bindingId);
5376
+ const expectedNotice = inbox?.failureCode === 'agent-task-timeout'
5377
+ ? timedOutTurnNotice : interruptedTurnNotice;
5378
+ if (record.intent.text !== expectedNotice || inbox === undefined || binding === undefined
5379
+ || !this.deliveryStore.canSendInterruptedInboxNotice(record)
5380
+ || !this.isInboundAuthorized(binding, inbox.envelope)) {
5381
+ throw new Error('interrupted Inbox authority changed');
5382
+ }
5383
+ const reply = this.policy.evaluate({
5384
+ subject: { kind: 'agent', id: binding.agentPreset, workspace: binding.workspace,
5385
+ principal: externalPrincipalId(binding.principal) },
5386
+ action: 'reply', resource: { kind: 'message', id: binding.id },
5387
+ context: { initiator: 'external' },
5388
+ });
5389
+ if (reply.effect !== 'allow')
5390
+ throw new Error('interrupted Inbox reply authority changed');
5391
+ }
5392
+ catch {
5393
+ return { outcome: 'not-sent', failureCode: 'interrupted-inbox-authority-revoked', retryable: false };
5394
+ }
5395
+ return undefined;
5396
+ }
5397
+ const metadata = record.intent.metadata;
5398
+ const nativeKeys = Object.keys(metadata ?? {}).filter(key => key.startsWith('dsh.native-notice'));
5399
+ if (nativeKeys.length > 0) {
5400
+ // Native adapter sends receive only an intent, so never bind a fabricated
5401
+ // outbox id into the goal-outcome sidecar lookup. A persisted record still
5402
+ // rejects any sidecar collision before native authority is considered.
5403
+ if (containsReservedLearningMetadata(metadata))
5404
+ return { outcome: 'not-sent', failureCode: 'native-notice-invalid', retryable: false };
5405
+ if (typeof record.id === 'string') {
5406
+ try {
5407
+ if (this.deliveryStore.getGoalOutcomeTarget(record.id) !== undefined)
5408
+ return { outcome: 'not-sent', failureCode: 'native-notice-invalid', retryable: false };
5409
+ }
5410
+ catch {
5411
+ return { outcome: 'not-sent', failureCode: 'goal-outcome-target-invalid', retryable: false };
5412
+ }
5413
+ }
5414
+ return this.#nativeNoticeGuard(record.intent, options);
5415
+ }
5416
+ if (Object.keys(metadata ?? {}).some(key => key.startsWith(linkedReportMetadataPrefix)))
5417
+ return this.linkedReportGuard(record);
5418
+ let goalTarget;
5419
+ try {
5420
+ goalTarget = this.deliveryStore.getGoalOutcomeTarget(record.id);
5421
+ }
5422
+ catch {
5423
+ return { outcome: 'not-sent', failureCode: 'goal-outcome-target-invalid', retryable: false };
5424
+ }
5425
+ if (goalTarget !== undefined || metadata?.['dsh.learning.kind'] === 'goal-outcome') {
5426
+ if (goalTarget === undefined) {
5427
+ return { outcome: 'not-sent', failureCode: 'goal-outcome-target-missing', retryable: false };
5428
+ }
5429
+ const locator = goalTarget.locator;
5430
+ const proof = goalTarget.proof;
5431
+ const binding = this.deliveryStore.getBinding(record.intent.bindingId);
5432
+ const owner = binding === undefined ? undefined : this.deliveryStore.getPrincipal(binding.principal);
5433
+ try {
5434
+ if (proof.receipt.completedAt > Date.now() || proof.receipt.validUntil <= Date.now()) {
5435
+ throw new Error('goal outcome receipt expired');
4214
5436
  }
4215
5437
  // One captured service reference must both issue and resolve the
4216
5438
  // process-local capability. A replacement cannot inherit authority.
@@ -4248,7 +5470,7 @@ export class AssistantDeliveryService extends Service {
4248
5470
  }
4249
5471
  return undefined;
4250
5472
  }
4251
- #nativeNoticeGuard(intent) {
5473
+ #nativeNoticeGuard(intent, options = { withdrawal: true }) {
4252
5474
  const metadata = intent.metadata;
4253
5475
  const nativeKeys = Object.keys(metadata ?? {}).filter(key => key.startsWith('dsh.native-notice'));
4254
5476
  if (nativeKeys.length === 0 || containsReservedLearningMetadata(metadata))
@@ -4274,6 +5496,30 @@ export class AssistantDeliveryService extends Service {
4274
5496
  catch {
4275
5497
  return { outcome: 'not-sent', failureCode: 'native-notice-policy-revoked', retryable: false };
4276
5498
  }
5499
+ const withdrawable = metadata['dsh.native-notice.withdrawable'];
5500
+ const authority = metadata['dsh.native-notice.withdrawalAuthority'];
5501
+ if (withdrawable !== undefined || authority !== undefined) {
5502
+ if (withdrawable !== 'v1' || typeof authority !== 'string' || !ownerNotificationAuthorityPattern.test(authority))
5503
+ return { outcome: 'not-sent', failureCode: 'native-notice-invalid', retryable: false };
5504
+ if (options.withdrawal) {
5505
+ // Fail closed: without a live check that the authority's current
5506
+ // service still vouches for, the notice waits (retryable, bounded by
5507
+ // its expiry) instead of sending. A foreign registration under another
5508
+ // authority is a different key and never reaches this notice.
5509
+ let withdrawn;
5510
+ try {
5511
+ const live = this.ownerNotificationWithdrawals.get(ownerNotificationWithdrawalKey(authority, canonicalBackgroundSourceId(metadata['dsh.native-notice.sourceId'])));
5512
+ if (live === undefined || !this.withdrawalVouched(live.authority, live.registration))
5513
+ return { outcome: 'not-sent', failureCode: 'native-notice-withdrawal-unavailable', retryable: true };
5514
+ withdrawn = live.registration.isWithdrawn(intent.idempotencyKey);
5515
+ }
5516
+ catch {
5517
+ return { outcome: 'not-sent', failureCode: 'native-notice-withdrawal-unavailable', retryable: true };
5518
+ }
5519
+ if (withdrawn !== false)
5520
+ return { outcome: 'not-sent', failureCode: 'native-notice-withdrawn', retryable: false };
5521
+ }
5522
+ }
4277
5523
  return undefined;
4278
5524
  }
4279
5525
  #notificationProviderId(intent) { return `native-notice:${createHash('sha256').update(intent.idempotencyKey).update('\u0000').update(intent.bindingId).update('\u0000').update(intent.text).digest('hex')}`; }
@@ -4282,6 +5528,15 @@ export class AssistantDeliveryService extends Service {
4282
5528
  this.assertActive();
4283
5529
  if (!Number.isSafeInteger(input.expiresAt) || input.expiresAt <= Date.now() || typeof input.text !== 'string' || input.text.length === 0 || input.text.length > 16_384)
4284
5530
  throw new AssistantDeliveryError('policy-denied', 'owner notification is invalid or expired');
5531
+ if (input.withdrawable !== undefined && typeof input.withdrawable !== 'boolean')
5532
+ throw new AssistantDeliveryError('policy-denied', 'owner notification is invalid or expired');
5533
+ if (input.withdrawalHandle !== undefined && input.withdrawable !== true)
5534
+ throw new AssistantDeliveryError('policy-denied', 'owner notification is invalid or expired');
5535
+ // A withdrawable notice must present a live handle for its own source; the
5536
+ // notice is then bound to that handle's authority, which alone controls it.
5537
+ const withdrawal = input.withdrawable === true ? this.liveWithdrawal(input.withdrawalHandle, input.sourceId) : undefined;
5538
+ if (input.withdrawable === true && withdrawal === undefined)
5539
+ throw new AssistantDeliveryError('policy-denied', 'owner notification withdrawal is not registered for this source');
4285
5540
  const resolved = this.resolveOwnerRoute(input.ownerRouteId);
4286
5541
  const binding = resolved.binding;
4287
5542
  const owner = this.deliveryStore.getPrincipal(binding.principal);
@@ -4291,8 +5546,251 @@ export class AssistantDeliveryService extends Service {
4291
5546
  return this.enqueueBackgroundAuthorized({ sourceId: input.sourceId, workspace: binding.workspace, bindingId: binding.id, idempotencyKey: input.idempotencyKey, text: input.text, format: 'plain', metadata: {
4292
5547
  '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,
4293
5548
  '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,
5549
+ ...(withdrawal !== undefined ? { 'dsh.native-notice.withdrawable': 'v1', 'dsh.native-notice.withdrawalAuthority': withdrawal.authority } : {}),
5550
+ } }, undefined, { lane: 'owner-notification', envelope: input.attention });
5551
+ }
5552
+ /**
5553
+ * Register the send-time withdrawal check for one owner-notice source (see
5554
+ * `OwnerNotificationWithdrawal`). The service in the `authority` slot must
5555
+ * vouch for the registration; one live check per (authority, source). A
5556
+ * registration its authority no longer vouches for (owner restarted or
5557
+ * replaced) is superseded by the next vouched one, so the legitimate owner
5558
+ * regains control of notices already queued under its authority. After
5559
+ * `unregister`, those notices wait instead of sending.
5560
+ */
5561
+ registerOwnerNotificationWithdrawal(registration) {
5562
+ this.assertActive();
5563
+ if (!registration || registration.protocol !== ownerNotificationWithdrawalProtocol || typeof registration.isWithdrawn !== 'function'
5564
+ || typeof registration.sourceId !== 'string' || typeof registration.authority !== 'string' || !ownerNotificationAuthorityPattern.test(registration.authority)) {
5565
+ throw new AssistantDeliveryError('runtime-conflict', 'invalid owner notification withdrawal');
5566
+ }
5567
+ const authority = registration.authority;
5568
+ const sourceId = canonicalBackgroundSourceId(registration.sourceId);
5569
+ if (!this.withdrawalVouched(authority, registration))
5570
+ throw new AssistantDeliveryError('runtime-conflict', 'owner notification withdrawal is not owned by its authority');
5571
+ const key = ownerNotificationWithdrawalKey(authority, sourceId);
5572
+ const existing = this.ownerNotificationWithdrawals.get(key);
5573
+ if (existing !== undefined) {
5574
+ if (this.withdrawalVouched(existing.authority, existing.registration))
5575
+ throw new AssistantDeliveryError('runtime-conflict', 'owner notification withdrawal already registered');
5576
+ this.ownerNotificationWithdrawalHandles.delete(existing.handle);
5577
+ }
5578
+ const handle = randomBytes(32).toString('base64url');
5579
+ const entry = Object.freeze({ handle, authority, sourceId, registration });
5580
+ this.ownerNotificationWithdrawals.set(key, entry);
5581
+ this.ownerNotificationWithdrawalHandles.set(handle, key);
5582
+ let live = true;
5583
+ return Object.freeze({ handle, unregister: () => {
5584
+ if (!live)
5585
+ return;
5586
+ live = false;
5587
+ this.ownerNotificationWithdrawalHandles.delete(handle);
5588
+ if (this.ownerNotificationWithdrawals.get(key) === entry)
5589
+ this.ownerNotificationWithdrawals.delete(key);
4294
5590
  } });
4295
5591
  }
5592
+ /** The service now in `authority`'s slot vouches for this exact registration object. */
5593
+ withdrawalVouched(authority, registration) {
5594
+ try {
5595
+ const owner = this.context.get(authority, false);
5596
+ return typeof owner?.ownsOwnerNotificationWithdrawal === 'function' && owner.ownsOwnerNotificationWithdrawal(registration) === true;
5597
+ }
5598
+ catch {
5599
+ return false;
5600
+ }
5601
+ }
5602
+ /** Live, still-vouched registration behind `handle`, for exactly `sourceId`. */
5603
+ liveWithdrawal(handle, sourceId) {
5604
+ if (typeof handle !== 'string')
5605
+ return undefined;
5606
+ const key = this.ownerNotificationWithdrawalHandles.get(handle);
5607
+ const entry = key === undefined ? undefined : this.ownerNotificationWithdrawals.get(key);
5608
+ if (entry === undefined || entry.handle !== handle || !this.withdrawalVouched(entry.authority, entry.registration))
5609
+ return undefined;
5610
+ let canonical;
5611
+ try {
5612
+ canonical = canonicalBackgroundSourceId(sourceId);
5613
+ }
5614
+ catch {
5615
+ return undefined;
5616
+ }
5617
+ return canonical === entry.sourceId ? entry : undefined;
5618
+ }
5619
+ /**
5620
+ * Read-only lookup of an owner notice by idempotency key. The caller
5621
+ * presents its withdrawal handle; the lookup is scoped to that handle's
5622
+ * source and authority. A key held by another source, another authority,
5623
+ * a non-withdrawable notice or a non-notice record reads as absent. Never
5624
+ * returns text or metadata.
5625
+ */
5626
+ inspectOwnerNotification(input) {
5627
+ this.assertActive();
5628
+ if (typeof input?.idempotencyKey !== 'string' || input.idempotencyKey.length === 0 || input.idempotencyKey.length > 256)
5629
+ throw new AssistantDeliveryError('runtime-conflict', 'invalid owner notification lookup');
5630
+ const withdrawal = this.liveWithdrawal(input.withdrawalHandle, input.sourceId);
5631
+ if (withdrawal === undefined)
5632
+ throw new AssistantDeliveryError('runtime-conflict', 'owner notification withdrawal is not registered for this source');
5633
+ const record = this.deliveryStore.getOutboxByIdempotencyKey(input.idempotencyKey);
5634
+ const metadata = record?.intent.metadata;
5635
+ if (record === undefined || metadata?.['dsh.native-notice'] !== 'v1' || typeof metadata['dsh.native-notice.sourceId'] !== 'string')
5636
+ return undefined;
5637
+ let stored;
5638
+ try {
5639
+ stored = canonicalBackgroundSourceId(metadata['dsh.native-notice.sourceId']);
5640
+ }
5641
+ catch {
5642
+ return undefined;
5643
+ }
5644
+ if (stored !== withdrawal.sourceId)
5645
+ return undefined;
5646
+ // A row this authority did not queue (another authority, or no withdrawal
5647
+ // binding at all) reads as absent, so the caller never adopts it.
5648
+ if (metadata['dsh.native-notice.withdrawalAuthority'] !== withdrawal.authority)
5649
+ return undefined;
5650
+ return Object.freeze({ outboxId: record.id, status: record.status });
5651
+ }
5652
+ /**
5653
+ * Register the slice-5 reply consumer for one owner-notice source (see
5654
+ * `OwnerNotificationReply`). The service in the `authority` slot must vouch
5655
+ * for the exact object; one live consumer per (authority, source). A
5656
+ * registration its authority no longer vouches for is superseded by the
5657
+ * next vouched one, so a restarted owner regains its replies without a new
5658
+ * notice. Registering never sends anything and grants no send authority.
5659
+ */
5660
+ registerOwnerNotificationReply(registration) {
5661
+ this.assertActive();
5662
+ if (!registration || registration.protocol !== ownerNotificationReplyProtocol
5663
+ || typeof registration.authority !== 'string' || !ownerNotificationAuthorityPattern.test(registration.authority)
5664
+ || typeof registration.sourceId !== 'string' || typeof registration.idempotencyKeyPrefix !== 'string'
5665
+ || registration.idempotencyKeyPrefix.length < 1 || registration.idempotencyKeyPrefix.length > 128
5666
+ || typeof registration.consume !== 'function') {
5667
+ throw new AssistantDeliveryError('runtime-conflict', 'invalid owner notification reply consumer');
5668
+ }
5669
+ const authority = registration.authority;
5670
+ const sourceId = canonicalBackgroundSourceId(registration.sourceId);
5671
+ if (!this.replyVouched(authority, registration))
5672
+ throw new AssistantDeliveryError('runtime-conflict', 'owner notification reply is not owned by its authority');
5673
+ const key = ownerNotificationWithdrawalKey(authority, sourceId);
5674
+ const existing = this.ownerNotificationReplies.get(key);
5675
+ if (existing !== undefined) {
5676
+ if (this.replyVouched(existing.authority, existing.registration))
5677
+ throw new AssistantDeliveryError('runtime-conflict', 'owner notification reply already registered');
5678
+ this.ownerNotificationReplies.delete(key);
5679
+ }
5680
+ const entry = Object.freeze({ authority, sourceId, registration });
5681
+ this.ownerNotificationReplies.set(key, entry);
5682
+ let live = true;
5683
+ return Object.freeze({ unregister: () => {
5684
+ if (!live)
5685
+ return;
5686
+ live = false;
5687
+ if (this.ownerNotificationReplies.get(key) === entry)
5688
+ this.ownerNotificationReplies.delete(key);
5689
+ } });
5690
+ }
5691
+ /** The service now in `authority`'s slot vouches for this exact reply registration. */
5692
+ replyVouched(authority, registration) {
5693
+ try {
5694
+ const owner = this.context.get(authority, false);
5695
+ return typeof owner?.ownsOwnerNotificationReply === 'function' && owner.ownsOwnerNotificationReply(registration) === true;
5696
+ }
5697
+ catch {
5698
+ return false;
5699
+ }
5700
+ }
5701
+ /**
5702
+ * Slice 5 matcher: bind one authenticated inbound text reply to exactly one
5703
+ * accepted/delivered/read owner notice through `replyToProviderMessageId`,
5704
+ * require the live registered consumer's source and idempotency-key prefix,
5705
+ * an active owner binding/principal with the notice's frozen owner lineage,
5706
+ * and an `allow` external `answer-attention-question` Policy decision. On
5707
+ * `applied` the Inbox is marked processed; `transient` leaves it pending for
5708
+ * a retry. Everything else falls through to ordinary inbound handling.
5709
+ */
5710
+ inspectOwnerQuestionReply(inbox, binding) {
5711
+ this.assertActive();
5712
+ const envelope = inbox.envelope;
5713
+ const replyTo = envelope.metadata?.replyToProviderMessageId;
5714
+ if (typeof replyTo !== 'string' || replyTo.length === 0 || envelope.kind !== 'text' || (envelope.attachments?.length ?? 0) > 0)
5715
+ return 'none';
5716
+ if (typeof envelope.text !== 'string' || envelope.text.length === 0 || Buffer.byteLength(envelope.text, 'utf8') > 256)
5717
+ return 'none';
5718
+ // The reply must come from the active owner binding itself, not another
5719
+ // conversation that happens to quote the provider message id.
5720
+ if (binding.status !== 'active' || externalPrincipalId(binding.principal) !== externalPrincipalId(envelope.principal)
5721
+ || workItemReplyCanonical(envelope.conversation) !== workItemReplyCanonical(binding.conversation))
5722
+ return 'none';
5723
+ const active = this.deliveryStore.getActiveBinding(binding.conversation);
5724
+ if (active?.id !== binding.id)
5725
+ return 'none';
5726
+ const owner = this.deliveryStore.getPrincipal(binding.principal);
5727
+ if (owner?.status !== 'active' || owner.role !== 'owner')
5728
+ return 'none';
5729
+ let outbox;
5730
+ try {
5731
+ outbox = this.deliveryStore.getUniqueOutboxByProviderMessage(inbox.channel, inbox.account, replyTo);
5732
+ }
5733
+ catch {
5734
+ return 'none';
5735
+ }
5736
+ if (outbox === undefined || !['accepted', 'delivered', 'read'].includes(outbox.status))
5737
+ return 'none';
5738
+ const metadata = outbox.intent.metadata;
5739
+ if (metadata?.['dsh.native-notice'] !== 'v1' || typeof metadata['dsh.native-notice.sourceId'] !== 'string'
5740
+ || typeof metadata['dsh.native-notice.withdrawalAuthority'] !== 'string')
5741
+ return 'none';
5742
+ let sourceId;
5743
+ try {
5744
+ sourceId = canonicalBackgroundSourceId(metadata['dsh.native-notice.sourceId']);
5745
+ }
5746
+ catch {
5747
+ return 'none';
5748
+ }
5749
+ const entry = this.ownerNotificationReplies.get(ownerNotificationWithdrawalKey(metadata['dsh.native-notice.withdrawalAuthority'], sourceId));
5750
+ if (entry === undefined || !this.replyVouched(entry.authority, entry.registration))
5751
+ return 'none';
5752
+ const registration = entry.registration;
5753
+ if (!outbox.intent.idempotencyKey.startsWith(registration.idempotencyKeyPrefix))
5754
+ return 'none';
5755
+ if (workItemReplyCanonical(outbox.intent.target.conversation) !== workItemReplyCanonical(envelope.conversation)
5756
+ || workItemReplyCanonical(outbox.intent.target.principal) !== workItemReplyCanonical(envelope.principal))
5757
+ return 'none';
5758
+ // The notice froze an exact owner row; a re-paired/replaced owner does not
5759
+ // inherit permission to answer the old owner's question.
5760
+ if (metadata['dsh.native-notice.ownerRecordId'] !== owner.id
5761
+ || metadata['dsh.native-notice.ownerVersion'] !== String(owner.version))
5762
+ return 'none';
5763
+ const decision = this.policy.evaluate({
5764
+ subject: { kind: 'external', id: externalPrincipalId(binding.principal), workspace: binding.workspace, principal: externalPrincipalId(binding.principal) },
5765
+ action: 'answer-attention-question', resource: { kind: 'message', id: binding.id }, context: { initiator: 'external' },
5766
+ });
5767
+ if (decision.effect !== 'allow')
5768
+ return 'none';
5769
+ let outcome;
5770
+ try {
5771
+ outcome = registration.consume(Object.freeze({
5772
+ inboxId: inbox.id, envelopeHash: inbox.envelopeHash, outboxId: outbox.id, providerMessageId: replyTo,
5773
+ idempotencyKey: outbox.intent.idempotencyKey, sourceId, principalId: externalPrincipalId(binding.principal),
5774
+ text: envelope.text, receivedAt: inbox.receivedAt,
5775
+ }));
5776
+ }
5777
+ catch {
5778
+ return 'transient';
5779
+ }
5780
+ if (outcome === 'ignored')
5781
+ return 'none';
5782
+ if (outcome !== 'applied')
5783
+ return 'transient';
5784
+ try {
5785
+ this.deliveryStore.markInboxProcessed(inbox.id, binding.id);
5786
+ }
5787
+ catch {
5788
+ // The consumer's own durable effect already committed; if recording that
5789
+ // here fails, never fall through and send the authorization to the Agent.
5790
+ return this.deliveryStore.getInbox(inbox.id)?.status === 'processed' ? 'handled' : 'transient';
5791
+ }
5792
+ return 'handled';
5793
+ }
4296
5794
  /**
4297
5795
  * Dedicated Automation result lane. Every learning field is derived from an
4298
5796
  * authoritative Automations receipt; callers cannot supply metadata,
@@ -4349,7 +5847,7 @@ export class AssistantDeliveryService extends Service {
4349
5847
  if (Buffer.byteLength(text, 'utf8') > this.config.maxTextBytes) {
4350
5848
  throw new AssistantDeliveryError('runtime-conflict', 'Automation result exceeds the delivery text limit');
4351
5849
  }
4352
- return this.deliveryStore.enqueue({
5850
+ return this.auditAttention('automation-result', proof.automationId, { lanePurpose: { purpose: 'result' } }, this.deliveryStore.enqueue({
4353
5851
  idempotencyKey: `automation:${proof.occurrenceId}:${binding.id}`,
4354
5852
  bindingId: binding.id,
4355
5853
  target: { conversation: binding.conversation, principal: binding.principal },
@@ -4366,7 +5864,7 @@ export class AssistantDeliveryService extends Service {
4366
5864
  'dsh.learning.outputDigest': proof.outputDigest,
4367
5865
  'dsh.learning.proofDigest': proof.proofDigest,
4368
5866
  }),
4369
- });
5867
+ }));
4370
5868
  }
4371
5869
  /** Resolve the current generation of one exact Host-owned owner route. */
4372
5870
  resolveOwnerRoute(authorityIdInput) {
@@ -4389,11 +5887,68 @@ export class AssistantDeliveryService extends Service {
4389
5887
  snapshot: ownerRouteBindingSnapshot(authority, binding),
4390
5888
  };
4391
5889
  }
5890
+ /**
5891
+ * Read-only Host observation of the configured owner, even after its binding
5892
+ * has been revoked. Missing routes and transient failures are not revocation.
5893
+ */
5894
+ inspectOwnerRouteState(input) {
5895
+ this.assertActive();
5896
+ let authorityId;
5897
+ try {
5898
+ authorityId = canonicalOwnerRouteId(input.authorityId);
5899
+ }
5900
+ catch {
5901
+ return Object.freeze({ status: 'unavailable', reason: 'authority-invalid' });
5902
+ }
5903
+ const authority = this.ownerRoutes.get(authorityId);
5904
+ if (authority === undefined) {
5905
+ return Object.freeze({ status: 'unavailable', reason: 'authority-unconfigured' });
5906
+ }
5907
+ const principalId = externalPrincipalId(authority.principal);
5908
+ // Equality with the bounded, validated authority also excludes malformed
5909
+ // scope values. Match validateOwnerRoute's NFC/trim normalization first.
5910
+ const text = (value) => typeof value === 'string'
5911
+ ? value.normalize('NFC').trim() : undefined;
5912
+ if (text(input.principalId) !== principalId || text(input.workspace) !== authority.workspace
5913
+ || text(input.agentPreset) !== authority.agentPreset) {
5914
+ return Object.freeze({ status: 'unavailable', reason: 'scope-mismatch' });
5915
+ }
5916
+ try {
5917
+ // Do not resolve an active binding before checking the durable owner:
5918
+ // revokePrincipal/handoffOwner revoke that binding in the same transaction.
5919
+ const owner = this.deliveryStore.getPrincipal(authority.principal);
5920
+ if (owner?.role !== 'owner' || externalPrincipalId(owner.principal) !== principalId) {
5921
+ return Object.freeze({ status: 'unavailable', reason: 'owner-unavailable' });
5922
+ }
5923
+ if (owner.status === 'revoked') {
5924
+ return Object.freeze({ status: 'revoked', authorityId,
5925
+ authorityHash: ownerRouteAuthorityHash(authority), principalId,
5926
+ principalRecordId: owner.id, principalVersion: owner.version });
5927
+ }
5928
+ return Object.freeze({ status: 'active', receipt: this.validateOwnerRoute(input) });
5929
+ }
5930
+ catch (error) {
5931
+ return Object.freeze({ status: 'unavailable', reason: error instanceof AssistantDeliveryError && error.code === 'missing-binding'
5932
+ ? 'route-unavailable' : 'inspection-unavailable' });
5933
+ }
5934
+ }
4392
5935
  /**
4393
5936
  * Revalidate Recovery/other Host work against the live owner route without
4394
5937
  * exposing a session id, conversation id, user message or delivery content.
4395
5938
  */
4396
5939
  validateOwnerRoute(input) {
5940
+ return this.ownerRouteReceipt(input).receipt;
5941
+ }
5942
+ /**
5943
+ * Host-only: the same receipt as `validateOwnerRoute`, plus the id of the
5944
+ * exact active binding it was resolved from (one resolve, so both describe
5945
+ * the same route state). Not an Agent tool; same `missing-binding` errors.
5946
+ */
5947
+ inspectOwnerRouteBinding(input) {
5948
+ const { receipt, bindingId } = this.ownerRouteReceipt(input);
5949
+ return Object.freeze({ protocol: 'assistant-delivery/owner-route-binding/v1', receipt, bindingId });
5950
+ }
5951
+ ownerRouteReceipt(input) {
4397
5952
  this.assertActive();
4398
5953
  const resolved = this.resolveOwnerRoute(input.authorityId);
4399
5954
  const authority = this.ownerRoutes.get(resolved.authorityId);
@@ -4423,7 +5978,7 @@ export class AssistantDeliveryService extends Service {
4423
5978
  || agentPreset !== authority.agentPreset) {
4424
5979
  throw new AssistantDeliveryError('missing-binding', 'owner route authority does not match the requested scope');
4425
5980
  }
4426
- return Object.freeze({
5981
+ const receipt = Object.freeze({
4427
5982
  receiptVersion: 2,
4428
5983
  authorityId: resolved.authorityId,
4429
5984
  authorityHash: resolved.snapshot.authorityHash,
@@ -4435,6 +5990,7 @@ export class AssistantDeliveryService extends Service {
4435
5990
  bindingVersion: resolved.snapshot.bindingVersion,
4436
5991
  generation: resolved.snapshot.generation,
4437
5992
  });
5993
+ return Object.freeze({ receipt, bindingId: resolved.snapshot.bindingId });
4438
5994
  }
4439
5995
  /** Host-only proof that the live caller Agent belongs to the current owner route. */
4440
5996
  validateOwnerAgentForRoute(agent, authorityId) {
@@ -4494,6 +6050,164 @@ export class AssistantDeliveryService extends Service {
4494
6050
  throw error;
4495
6051
  }
4496
6052
  }
6053
+ /**
6054
+ * Host-only, READ-ONLY proof that the live caller Agent is bound to an
6055
+ * active `linked` principal whose `linked_to_id` is the CURRENT owner row of
6056
+ * this owner route. The receipt has a distinct protocol and no
6057
+ * `receiptVersion`, so v2 owner checks reject it; it never grants owner,
6058
+ * approval, continuation or mutation authority. Every call re-reads the
6059
+ * route, the root owner, the alias binding and the alias principal twice.
6060
+ */
6061
+ inspectOwnerAliasRoute(agent, authorityId) {
6062
+ if (!this.active || typeof agent !== 'object' || agent === null || agent.session === undefined
6063
+ || typeof authorityId !== 'string' || authorityId.length === 0
6064
+ || Buffer.byteLength(authorityId) > toolApprovalIdentityBytes)
6065
+ return undefined;
6066
+ const agents = this.context.get('agents');
6067
+ const sessions = this.context.get('sessions');
6068
+ if (agents === undefined || sessions === undefined)
6069
+ return undefined;
6070
+ const sessionId = String(agent.session.id);
6071
+ const liveCaller = () => String(agent.id) === sessionId
6072
+ && agents.get(agent.id) === agent
6073
+ && sessions.get(agent.id) === agent.session;
6074
+ if (!liveCaller())
6075
+ return undefined;
6076
+ const read = () => {
6077
+ const route = this.resolveOwnerRoute(authorityId);
6078
+ const authority = this.ownerRoutes.get(route.authorityId);
6079
+ const root = this.deliveryStore.getPrincipal(route.binding.principal);
6080
+ if (authority === undefined || root?.status !== 'active' || root.role !== 'owner'
6081
+ || JSON.stringify(root.principal) !== JSON.stringify(route.binding.principal))
6082
+ return undefined;
6083
+ const binding = this.deliveryStore.getBindingBySession(sessionId);
6084
+ if (binding?.status !== 'active' || binding.sessionId !== sessionId || binding.id === route.binding.id
6085
+ || this.deliveryStore.getActiveBinding(binding.conversation)?.id !== binding.id
6086
+ || binding.workspace !== authority.workspace || binding.agentPreset !== authority.agentPreset
6087
+ || agent.session.header.cwd !== binding.workspace
6088
+ || agent.session.header.agentPreset !== binding.agentPreset)
6089
+ return undefined;
6090
+ const alias = this.deliveryStore.getPrincipal(binding.principal);
6091
+ if (alias?.status !== 'active' || alias.role !== 'linked'
6092
+ || typeof alias.linkedToId !== 'string' || alias.linkedToId !== root.id || alias.id === root.id
6093
+ || JSON.stringify(alias.principal) !== JSON.stringify(binding.principal))
6094
+ return undefined;
6095
+ return Object.freeze({
6096
+ protocol: 'assistant-delivery/owner-alias-receipt/v1',
6097
+ authorityId: route.authorityId,
6098
+ authorityHash: route.snapshot.authorityHash,
6099
+ workspace: authority.workspace,
6100
+ agentPreset: authority.agentPreset,
6101
+ alias: Object.freeze({
6102
+ principalId: externalPrincipalId(alias.principal),
6103
+ principalRecordId: alias.id,
6104
+ principalVersion: alias.version,
6105
+ bindingId: binding.id,
6106
+ bindingVersion: binding.version,
6107
+ bindingGeneration: binding.generation,
6108
+ sessionId,
6109
+ }),
6110
+ root: Object.freeze({
6111
+ principalId: externalPrincipalId(root.principal),
6112
+ principalRecordId: root.id,
6113
+ principalVersion: root.version,
6114
+ bindingVersion: route.snapshot.bindingVersion,
6115
+ generation: route.snapshot.generation,
6116
+ }),
6117
+ });
6118
+ };
6119
+ try {
6120
+ const first = read();
6121
+ if (first === undefined)
6122
+ return undefined;
6123
+ const current = read();
6124
+ return current !== undefined && liveCaller()
6125
+ && acceptanceCanonicalJson(current) === acceptanceCanonicalJson(first) ? first : undefined;
6126
+ }
6127
+ catch (error) {
6128
+ if (error instanceof AssistantDeliveryError && error.code === 'missing-binding')
6129
+ return undefined;
6130
+ throw error;
6131
+ }
6132
+ }
6133
+ /**
6134
+ * Host-only proof of one live, unsettled native tool call in the alias's
6135
+ * exact claimed human Inbox turn. Read-only; it is NOT an owner foreground
6136
+ * proof (`assistant-delivery/foreground-tool-call/v1`) and must never be
6137
+ * accepted where one is required.
6138
+ */
6139
+ inspectOwnerAliasForegroundToolCall(input) {
6140
+ if (!input || typeof input !== 'object'
6141
+ || typeof input.authorityId !== 'string' || input.authorityId.length === 0
6142
+ || typeof input.callId !== 'string' || input.callId.length === 0
6143
+ || typeof input.toolName !== 'string' || input.toolName.length === 0
6144
+ || typeof input.argumentsJson !== 'string'
6145
+ || Buffer.byteLength(input.authorityId) > toolApprovalIdentityBytes
6146
+ || Buffer.byteLength(input.callId) > toolApprovalIdentityBytes
6147
+ || Buffer.byteLength(input.toolName) > toolApprovalIdentityBytes
6148
+ || Buffer.byteLength(input.argumentsJson) > toolApprovalArgumentBytes)
6149
+ return undefined;
6150
+ const agent = input.agent;
6151
+ const receipt = this.inspectOwnerAliasRoute(agent, input.authorityId);
6152
+ if (receipt === undefined)
6153
+ return undefined;
6154
+ const turn = this.currentAliasTurn(agent, receipt);
6155
+ if (turn === undefined)
6156
+ return undefined;
6157
+ const call = inspectUnsettledNativeCall(agent.session.snapshotEvents(), turn.turn, input.callId, input.toolName, input.argumentsJson);
6158
+ if (call === undefined)
6159
+ return undefined;
6160
+ const current = this.inspectOwnerAliasRoute(agent, input.authorityId);
6161
+ if (current === undefined || acceptanceCanonicalJson(current) !== acceptanceCanonicalJson(receipt)
6162
+ || this.currentAliasTurn(agent, receipt) !== turn)
6163
+ return undefined;
6164
+ return Object.freeze({ protocol: 'assistant-delivery/owner-alias-foreground-tool-call/v1',
6165
+ receipt, sourceInboxId: turn.sourceInboxId, sourceEventId: turn.sourceEventId, turn: turn.turn, call });
6166
+ }
6167
+ currentAliasTurn(agent, receipt) {
6168
+ const turn = this.aliasTurns.get(agent);
6169
+ if (turn === undefined || turn.sessionId !== receipt.alias.sessionId
6170
+ || turn.bindingId !== receipt.alias.bindingId || turn.bindingVersion !== receipt.alias.bindingVersion
6171
+ || turn.principalRecordId !== receipt.alias.principalRecordId || turn.principalVersion !== receipt.alias.principalVersion
6172
+ || turn.linkedToId !== receipt.root.principalRecordId)
6173
+ return undefined;
6174
+ const binding = this.deliveryStore.getBinding(turn.bindingId);
6175
+ const inbox = this.deliveryStore.getInbox(turn.sourceInboxId);
6176
+ if (binding?.status !== 'active' || binding.version !== turn.bindingVersion
6177
+ || binding.sessionId !== turn.sessionId
6178
+ || inbox?.status !== 'claimed' || inbox.bindingId !== binding.id
6179
+ || inbox.envelope.eventId !== turn.sourceEventId
6180
+ || !this.isInboundAuthorized(binding, inbox.envelope))
6181
+ return undefined;
6182
+ const events = agent.session.snapshotEvents();
6183
+ const started = events.some(event => event.type === 'turn/start' && event.data.turn === turn.turn);
6184
+ const ended = events.some(event => event.type === 'turn/end' && event.data.turn === turn.turn);
6185
+ return started && !ended ? turn : undefined;
6186
+ }
6187
+ captureAliasTurn(agent, sourceInput, turnInput) {
6188
+ if (!this.active || typeof sourceInput !== 'object' || sourceInput === null
6189
+ || !Number.isSafeInteger(turnInput) || turnInput < 0)
6190
+ return;
6191
+ const source = sourceInput;
6192
+ if (source.kind !== 'delivery' || typeof source.channel !== 'string'
6193
+ || typeof source.account !== 'string' || typeof source.eventId !== 'string')
6194
+ return;
6195
+ const sessionId = String(agent.session.id);
6196
+ const binding = this.deliveryStore.getBindingBySession(sessionId);
6197
+ const inbox = this.deliveryStore.getInboxByProviderEvent(source.channel, source.account, source.eventId);
6198
+ const principal = binding === undefined ? undefined : this.deliveryStore.getPrincipal(binding.principal);
6199
+ if (binding?.status !== 'active' || inbox?.status !== 'claimed' || inbox.bindingId !== binding.id
6200
+ || binding.sessionId !== sessionId || binding.workspace !== agent.session.header.cwd
6201
+ || binding.agentPreset !== agent.session.header.agentPreset
6202
+ || principal?.status !== 'active' || principal.role !== 'linked' || typeof principal.linkedToId !== 'string'
6203
+ || !this.isInboundAuthorized(binding, inbox.envelope))
6204
+ return;
6205
+ this.aliasTurns.set(agent, Object.freeze({
6206
+ sessionId, bindingId: binding.id, bindingVersion: binding.version,
6207
+ principalRecordId: principal.id, principalVersion: principal.version, linkedToId: principal.linkedToId,
6208
+ sourceEventId: source.eventId, sourceInboxId: inbox.id, turn: turnInput,
6209
+ }));
6210
+ }
4497
6211
  /** Host-only durable ordinary-turn metadata; never registered as a model tool. */
4498
6212
  listOwnerForegroundTaskSources(input) {
4499
6213
  this.assertActive();
@@ -4512,6 +6226,12 @@ export class AssistantDeliveryService extends Service {
4512
6226
  this.validateOwnerRoute(input);
4513
6227
  return this.deliveryStore.readOwnerForegroundTaskSource(input, this.ownerRoutes.get(input.authorityId));
4514
6228
  }
6229
+ /** Host-only bounded original input, independent of successful reply evidence. */
6230
+ readOwnerForegroundTaskInput(input) {
6231
+ this.assertActive();
6232
+ this.validateOwnerRoute(input);
6233
+ return this.deliveryStore.readOwnerForegroundTaskInput(input, this.ownerRoutes.get(input.authorityId));
6234
+ }
4515
6235
  /** Hold Delivery's owner/source writer fence across synchronous downstream adoption. */
4516
6236
  withOwnerForegroundTaskSourceFence(input, callback) {
4517
6237
  this.assertActive();
@@ -4524,6 +6244,135 @@ export class AssistantDeliveryService extends Service {
4524
6244
  this.validateOwnerRoute(input);
4525
6245
  return this.deliveryStore.withOwnerForegroundTaskSourcesFence(input, this.ownerRoutes.get(input.authorityId), callback);
4526
6246
  }
6247
+ /** One Delivery → Evaluation → WorkItems source fence; callback must be local and synchronous. */
6248
+ withOwnerLearningSourcesFence(input, callback) {
6249
+ this.assertActive();
6250
+ input = learningSourceData(input);
6251
+ if (typeof callback !== 'function' || !Array.isArray(input.sources) || input.sources.length > 1_000)
6252
+ throw new AssistantDeliveryError('runtime-conflict', 'invalid learning source batch');
6253
+ // Snapshot caller data before any peer callbacks. Never retain live accessors or mutable expectations.
6254
+ const sources = input.sources;
6255
+ const keys = new Set(), hex = /^[a-f0-9]{64}$/u;
6256
+ for (const source of sources) {
6257
+ if (!source || !['foreground', 'work-item'].includes(source.kind) || !hex.test(source.expectedSourceDigest))
6258
+ throw new AssistantDeliveryError('runtime-conflict', 'invalid learning source expectation');
6259
+ const id = source.kind === 'foreground' ? source.inboxId : source.itemId, key = `${source.kind}:${id}`;
6260
+ if (typeof id !== 'string' || !id || keys.has(key))
6261
+ throw new AssistantDeliveryError('runtime-conflict', 'learning source identities must be unique');
6262
+ keys.add(key);
6263
+ if (source.kind === 'work-item' && (typeof source.outcomeId !== 'string' || !source.outcomeId || !hex.test(source.expectedContentDigest)
6264
+ || !source.canonical || !Number.isSafeInteger(source.canonical.version) || source.canonical.version < 1 || !hex.test(source.canonical.digest)
6265
+ || !['achieved', 'not-achieved'].includes(source.canonical.objectiveStatus)))
6266
+ throw new AssistantDeliveryError('runtime-conflict', 'invalid work item learning expectation');
6267
+ }
6268
+ this.validateOwnerRoute(input);
6269
+ const evaluation = this.context.get('assistantEvaluation', false);
6270
+ if (!evaluation?.withTrustedCanonicalScopeWriterFence)
6271
+ throw new AssistantDeliveryError('runtime-conflict', 'canonical source fence is unavailable');
6272
+ const evaluationIdentity = workItemPeerIdentity(evaluation);
6273
+ return this.deliveryStore.withOwnerLearningSourceFence(input, this.ownerRoutes.get(input.authorityId), () => {
6274
+ const feedbackRegistration = sources.some(source => source.kind === 'work-item') ? this.currentWorkItemReplyConsumer() : undefined;
6275
+ const prepared = sources.map(source => {
6276
+ if (source.kind !== 'work-item')
6277
+ return undefined;
6278
+ const state = this.ownerWorkItemLearningTaskState({ ...input, outcomeId: source.outcomeId });
6279
+ return state ? { ...state, task: learningSourceData(state.task), revision: learningSourceData(state.revision), resolved: { ...state.resolved, proof: learningSourceData(state.resolved.proof) } } : undefined;
6280
+ });
6281
+ const peer = this.context.get('assistantWorkItems', false);
6282
+ const peerIdentity = workItemPeerIdentity(peer);
6283
+ const scope = evaluation.canonicalHostScope({ workspace: input.workspace, preset: input.agentPreset });
6284
+ const { scopeWatermark } = evaluation.listTrustedTaskLearningProjections({ scope, limit: 1 });
6285
+ const fenced = evaluation.withTrustedCanonicalScopeWriterFence({ scope, scopeWatermark }, () => {
6286
+ const active = prepared.flatMap((state, index) => state ? [{ state, index }] : []);
6287
+ const finish = (proofs) => {
6288
+ proofs = learningSourceData(proofs);
6289
+ if (!Array.isArray(proofs) || proofs.length !== active.length)
6290
+ throw new AssistantDeliveryError('runtime-conflict', 'work item proof batch is misaligned');
6291
+ if (!this.active || workItemPeerIdentity(this.context.get('assistantEvaluation', false)) !== evaluationIdentity
6292
+ || workItemPeerIdentity(this.context.get('assistantWorkItems', false)) !== peerIdentity)
6293
+ throw new AssistantDeliveryError('runtime-conflict', 'learning source provider changed');
6294
+ const owner = this.validateOwnerRoute(input);
6295
+ if (owner.authorityHash !== input.expectedOwner.authorityHash || owner.principalRecordId !== input.expectedOwner.principalRecordId
6296
+ || owner.principalVersion !== input.expectedOwner.principalVersion)
6297
+ throw new AssistantDeliveryError('runtime-conflict', 'learning source owner changed');
6298
+ const available = new Map();
6299
+ for (const [offset, { state, index }] of active.entries()) {
6300
+ const proof = proofs[offset], expectation = sources[index];
6301
+ if (expectation.kind !== 'work-item' || !proof || workItemReplyCanonical(proof) !== workItemReplyCanonical(state.resolved.proof))
6302
+ continue;
6303
+ const current = evaluation.getTrustedWorkItemLearningProjection({ scope, itemId: expectation.itemId });
6304
+ const revision = evaluation.inspectTrustedTaskOwnerRevision({ scope, outcomeId: expectation.outcomeId, principalRecordId: owner.principalRecordId, principalVersion: owner.principalVersion });
6305
+ if (state.resolved.peerIdentity !== peerIdentity || workItemReplyCanonical(current) !== workItemReplyCanonical(state.task.canonical)
6306
+ || workItemReplyCanonical(revision) !== workItemReplyCanonical(state.revision) || state.task.proofIdentity.itemId !== expectation.itemId
6307
+ || state.task.sourceDigest !== expectation.expectedSourceDigest || state.task.presentationDigest !== expectation.expectedContentDigest
6308
+ || !current?.objective || current.objective.outcomeId !== expectation.outcomeId || current.projection.version !== expectation.canonical.version
6309
+ || current.projection.digest !== expectation.canonical.digest || current.objective.status !== expectation.canonical.objectiveStatus)
6310
+ continue;
6311
+ try {
6312
+ this.workItemFeedbackFence(state.intent, true, state.resolved.registration);
6313
+ }
6314
+ catch {
6315
+ continue;
6316
+ }
6317
+ available.set(index, Object.freeze({ ...state.task, owner }));
6318
+ }
6319
+ // Every foreign owns/resolve callback has finished. Only native local source/canonical/Policy reads remain.
6320
+ const snapshots = Object.freeze(sources.map((source, index) => {
6321
+ if (source.kind === 'work-item') {
6322
+ let currentCanonical = evaluation.getTrustedWorkItemLearningProjection({ scope, itemId: source.itemId });
6323
+ const outcomeId = currentCanonical?.objective?.outcomeId;
6324
+ let ownerRevision = outcomeId === undefined ? undefined : evaluation.inspectTrustedTaskOwnerRevision({ scope, outcomeId, principalRecordId: owner.principalRecordId, principalVersion: owner.principalVersion });
6325
+ const intent = ownerRevision?.operationId === undefined ? undefined : this.deliveryStore.workItemFeedback.findOutcome(ownerRevision.outcomeId, ownerRevision.operationId);
6326
+ let authorized = false;
6327
+ if (feedbackRegistration && currentCanonical?.projection.subjectKind === 'work-item' && currentCanonical.projection.subjectRef === source.itemId
6328
+ && intent && intent.source.target.itemId === source.itemId && intent.source.owner.principalRecordId === owner.principalRecordId
6329
+ && intent.source.owner.principalVersion === owner.principalVersion) {
6330
+ try {
6331
+ this.workItemFeedbackFence(intent, true, feedbackRegistration);
6332
+ authorized = true;
6333
+ }
6334
+ catch { /* Temporarily unavailable sources do not expose another owner's canonical state. */ }
6335
+ }
6336
+ if (!authorized) {
6337
+ currentCanonical = undefined;
6338
+ ownerRevision = undefined;
6339
+ }
6340
+ return Object.freeze({ kind: 'work-item', itemId: source.itemId, task: authorized ? available.get(index) : undefined, currentCanonical, ownerRevision });
6341
+ }
6342
+ const content = this.deliveryStore.readOwnerForegroundTaskSource({ ...input, ...source }, this.ownerRoutes.get(input.authorityId));
6343
+ const ownedSource = this.deliveryStore.inspectOwnerForegroundTaskSource({ ...input, inboxId: source.inboxId }, this.ownerRoutes.get(input.authorityId));
6344
+ const currentCanonical = ownedSource === undefined ? undefined : evaluation.getTrustedForegroundLearningProjection({ scope, inboxId: source.inboxId });
6345
+ const outcomeId = currentCanonical?.objective?.outcomeId ?? currentCanonical?.execution?.outcomeId;
6346
+ const ownerRevision = outcomeId === undefined ? undefined : evaluation.inspectTrustedTaskOwnerRevision({ scope, outcomeId, principalRecordId: owner.principalRecordId, principalVersion: owner.principalVersion });
6347
+ const task = outcomeId === undefined ? undefined : this.inspectOwnerForegroundLearningTask({ ...input, outcomeId });
6348
+ return Object.freeze({ kind: 'foreground', inboxId: source.inboxId, content, currentCanonical, ownerRevision, task });
6349
+ }));
6350
+ return callback(snapshots);
6351
+ };
6352
+ if (!active.length)
6353
+ return finish([]);
6354
+ if (!peer?.withOwnerWorkItemFeedbackTargetsFence)
6355
+ throw new AssistantDeliveryError('runtime-conflict', 'work item batch source fence is unavailable');
6356
+ return peer.withOwnerWorkItemFeedbackTargetsFence(active.map(({ state }) => state.resolved.capability), finish);
6357
+ });
6358
+ if (!fenced.matched)
6359
+ throw new AssistantDeliveryError('runtime-conflict', 'canonical source scope changed');
6360
+ return fenced.value;
6361
+ });
6362
+ }
6363
+ /** Strict single-source wrapper: unavailable or changed sources never invoke the callback. */
6364
+ withOwnerWorkItemLearningTaskFence(input, callback) {
6365
+ input = learningSourceData(input);
6366
+ if (typeof callback !== 'function')
6367
+ throw new AssistantDeliveryError('runtime-conflict', 'invalid learning source callback');
6368
+ return this.withOwnerLearningSourcesFence({ ...input, sources: [{ kind: 'work-item', itemId: input.itemId, outcomeId: input.outcomeId,
6369
+ expectedSourceDigest: input.expectedSourceDigest, expectedContentDigest: input.expectedContentDigest, canonical: input.canonical }] }, sources => {
6370
+ const source = sources[0];
6371
+ if (!source || source.kind !== 'work-item' || !source.task)
6372
+ throw new AssistantDeliveryError('runtime-conflict', 'work item learning source is unavailable or changed');
6373
+ return callback(source.task);
6374
+ });
6375
+ }
4527
6376
  /** Host-only snapshot of the model selected for this owner's external conversation. */
4528
6377
  inspectOwnerModelSelection(input) {
4529
6378
  this.validateOwnerRoute(input);
@@ -4555,10 +6404,194 @@ export class AssistantDeliveryService extends Service {
4555
6404
  ...(selected.reasoningEffort === undefined ? {} : { reasoningEffort: selected.reasoningEffort }) });
4556
6405
  }
4557
6406
  /**
4558
- * Resolve one canonical foreground result against its actual owner lineage.
6407
+ * Resolve one canonical work-item result against its actual owner lineage.
4559
6408
  * Evaluation's workspace/preset scope alone does not establish ownership.
4560
6409
  * Historical Sessions remain eligible after /new, but not after owner ABA.
4561
6410
  */
6411
+ inspectOwnerWorkItemLearningTask(input) {
6412
+ return this.ownerWorkItemLearningTaskState(input)?.task;
6413
+ }
6414
+ ownerWorkItemLearningTaskState(input) {
6415
+ this.assertActive();
6416
+ const owner = this.validateOwnerRoute(input);
6417
+ const evaluation = this.context.get('assistantEvaluation', false);
6418
+ if (!evaluation || typeof evaluation.getTrustedWorkItemLearningProjection !== 'function')
6419
+ return undefined;
6420
+ const scope = evaluation.canonicalHostScope({ workspace: input.workspace, preset: input.agentPreset });
6421
+ const canonical = evaluation.getTrustedTaskLearningProjection({ scope, outcomeId: input.outcomeId });
6422
+ if (!canonical || canonical.projection.subjectKind !== 'work-item' || canonical.projection.disposition !== 'upsert' || !canonical.objective
6423
+ || canonical.objective.outcomeId !== input.outcomeId || canonical.objective.source.kind !== 'user-feedback' || canonical.objective.source.id !== 'assistant-delivery/typed-owner-feedback' || canonical.objective.status === 'unknown')
6424
+ return undefined;
6425
+ const revision = evaluation.inspectTrustedTaskOwnerRevision({ scope, outcomeId: input.outcomeId, principalRecordId: owner.principalRecordId, principalVersion: owner.principalVersion });
6426
+ if (!revision || revision.action === 'withdraw' || !revision.operationId || revision.outcomeId !== canonical.objective.outcomeId || revision.objectiveStatus !== canonical.objective.status)
6427
+ return undefined;
6428
+ const intent = this.deliveryStore.workItemFeedback.findOutcome(revision.outcomeId, revision.operationId);
6429
+ if (!intent || intent.source.target.itemId !== canonical.projection.subjectRef || intent.source.owner.principalRecordId !== owner.principalRecordId || intent.source.owner.principalVersion !== owner.principalVersion)
6430
+ return undefined;
6431
+ let resolved;
6432
+ try {
6433
+ resolved = this.workItemFeedbackProof(intent, true);
6434
+ }
6435
+ catch {
6436
+ return undefined;
6437
+ }
6438
+ if (!resolved)
6439
+ return undefined;
6440
+ const proof = resolved.proof;
6441
+ if (proof.owner.ownerRouteId !== owner.authorityId || proof.owner.principalId !== owner.principalId || !canonical.execution
6442
+ || canonical.execution.status !== proof.production.executionStatus
6443
+ || !canonical.execution.evidence.some(ref => ref.kind === 'work-item-production-proof' && ref.ref === proof.production.runId && ref.digest === proof.proofDigest)
6444
+ || !canonical.objective.evidence.some(ref => ref.kind === 'work-item-report' && ref.ref === `${proof.itemId}#${proof.reportRevision}` && ref.digest === proof.reportDigest)
6445
+ || !canonical.objective.evidence.some(ref => ref.kind === 'delivery-outbox' && ref.ref === intent.source.target.outboxId))
6446
+ return undefined;
6447
+ const { task, report, ...proofIdentity } = proof;
6448
+ const brief = redactOwnerLearningReply(task.brief, 4096), visibleReport = redactOwnerLearningReply(report.text, 4096), feedback = redactOwnerLearningReply(intent.source.inputText, 4096);
6449
+ const presentation = { brief: { ...brief, truncated: brief.truncated || task.briefTruncated }, report: { ...visibleReport, truncated: visibleReport.truncated || report.truncated }, feedback: { inboxId: intent.source.inboxId, ...feedback } };
6450
+ // Current route/session is an authorization check, never part of immutable historical source identity.
6451
+ const sourceDigest = workItemReplyDigest({ protocol: 'assistant-delivery/owner-work-item-learning-source/v1', sourceDigest: intent.sourceDigest, proofDigest: proof.proofDigest,
6452
+ projection: canonical.projection, ownerRevision: { version: revision.version, action: revision.action, operationId: revision.operationId } });
6453
+ try {
6454
+ const final = this.workItemFeedbackProof(intent, true), current = evaluation.getTrustedTaskLearningProjection({ scope, outcomeId: input.outcomeId });
6455
+ const currentRevision = evaluation.inspectTrustedTaskOwnerRevision({ scope, outcomeId: input.outcomeId, principalRecordId: owner.principalRecordId, principalVersion: owner.principalVersion });
6456
+ if (!final || final.proof.proofDigest !== proof.proofDigest || workItemReplyCanonical(current) !== workItemReplyCanonical(canonical)
6457
+ || workItemReplyCanonical(currentRevision) !== workItemReplyCanonical(revision) || workItemReplyCanonical(this.validateOwnerRoute(input)) !== workItemReplyCanonical(owner))
6458
+ return undefined;
6459
+ if (final.registration !== resolved.registration || final.peerIdentity !== resolved.peerIdentity
6460
+ || workItemPeerIdentity(this.context.get('assistantWorkItems', false)) !== final.peerIdentity
6461
+ || workItemPeerIdentity(this.context.get('assistantEvaluation', false)) !== workItemPeerIdentity(evaluation))
6462
+ return undefined;
6463
+ this.workItemFeedbackFence(intent, true, final.registration);
6464
+ }
6465
+ catch {
6466
+ return undefined;
6467
+ }
6468
+ const learningTask = Object.freeze({ protocol: 'assistant-delivery/owner-work-item-learning/v1', owner, canonical, judgement: 'owner-feedback',
6469
+ ownerRevision: { version: revision.version, action: revision.action, operationId: revision.operationId }, proofIdentity, sourceDigest, presentation, presentationDigest: workItemReplyDigest(presentation) });
6470
+ return { task: learningTask, intent, resolved, evaluation, revision };
6471
+ }
6472
+ /** Host-only complete historical objective; old feedback and Memory identities are not reinterpreted. */
6473
+ inspectOwnerWorkItemEngineeringContext(input) {
6474
+ try {
6475
+ return this.withOwnerWorkItemEngineeringContextSource(input, undefined, ({ task, manifest }) => Object.freeze({ task, manifest }));
6476
+ }
6477
+ catch {
6478
+ return undefined;
6479
+ }
6480
+ }
6481
+ /** Every page, including the last, revalidates the entire exact source frame. */
6482
+ readOwnerWorkItemEngineeringContext(input) {
6483
+ try {
6484
+ const read = engineeringContextData(input);
6485
+ return this.withOwnerWorkItemEngineeringContextSource(read, read, source => pageWorkItemEngineeringContext(source, source.material, read));
6486
+ }
6487
+ catch {
6488
+ return undefined;
6489
+ }
6490
+ }
6491
+ withOwnerWorkItemEngineeringContextFence(input, callback) {
6492
+ if (typeof callback !== 'function')
6493
+ throw new AssistantDeliveryError('runtime-conflict', 'engineering context callback is required');
6494
+ return this.withOwnerWorkItemEngineeringContextSource(input, input, ({ task, manifest, objective }) => callback(Object.freeze({ task, manifest, objective })));
6495
+ }
6496
+ withOwnerWorkItemEngineeringContextSource(input, expectation, callback) {
6497
+ this.assertActive();
6498
+ input = engineeringContextData(input);
6499
+ const expected = expectation === undefined ? undefined : engineeringContextData(expectation);
6500
+ const unavailable = () => { throw new AssistantDeliveryError('runtime-conflict', 'work item engineering context is unavailable or changed'); };
6501
+ const state = this.ownerWorkItemLearningTaskState(input);
6502
+ if (!state || state.task.proofIdentity.itemId !== input.itemId)
6503
+ return unavailable();
6504
+ const task = learningSourceData(state.task), proof = learningSourceData(state.resolved.proof), revision = learningSourceData(state.revision);
6505
+ const evaluation = state.evaluation, evaluationIdentity = workItemPeerIdentity(evaluation);
6506
+ const peer = this.context.get('assistantWorkItems', false);
6507
+ const peerIdentity = workItemPeerIdentity(peer);
6508
+ if (!peer?.issueOwnerWorkItemEngineeringTarget || !peer.resolveOwnerWorkItemEngineeringTarget || !peer.withOwnerWorkItemEngineeringTargetsFence
6509
+ || peerIdentity !== state.resolved.peerIdentity)
6510
+ return unavailable();
6511
+ // Discover the sealed root before acquiring Delivery. The final producer frame rechecks this exact opaque capability.
6512
+ const capability = peer.issueOwnerWorkItemEngineeringTarget(state.resolved.capability);
6513
+ if (capability === undefined)
6514
+ return unavailable();
6515
+ const material = validateWorkItemEngineeringMaterial(peer.resolveOwnerWorkItemEngineeringTarget(capability));
6516
+ if (material.itemId !== input.itemId || material.feedbackProofDigest !== proof.proofDigest)
6517
+ return unavailable();
6518
+ const root = material.nodes[0].input;
6519
+ return this.deliveryStore.withOwnerLearningSourceFence(input, this.ownerRoutes.get(input.authorityId), () => {
6520
+ const rootText = () => {
6521
+ const owner = this.validateOwnerRoute(input);
6522
+ const source = this.deliveryStore.inspectOwnerForegroundTaskSource({ ...input, inboxId: root.inboxId }, this.ownerRoutes.get(input.authorityId));
6523
+ const inbox = this.deliveryStore.getInbox(root.inboxId);
6524
+ if (!source || !inbox || inbox.envelope.kind !== 'text' || !inbox.envelope.text.trim() || (inbox.envelope.attachments?.length ?? 0) > 0 || source.sourceDigest !== root.sourceDigest || source.execution.executionRef !== root.executionRef
6525
+ || source.completionSequence !== root.completionSequence || source.binding.id !== root.binding.id || source.binding.version !== root.binding.version
6526
+ || source.binding.generation !== root.binding.generation || owner.generation < root.binding.generation
6527
+ || source.owner.principalRecordId !== owner.principalRecordId || source.owner.principalVersion !== owner.principalVersion)
6528
+ return unavailable();
6529
+ return inbox.envelope.text;
6530
+ };
6531
+ const ownerInputs = () => {
6532
+ const text = rootText(), owner = this.validateOwnerRoute(input);
6533
+ let previous = root.binding;
6534
+ for (const [index, node] of material.nodes.entries()) {
6535
+ if (index === 0)
6536
+ continue;
6537
+ if (node.planProtocol !== 'assistant-work-items/owner-continuation-plan/v2' || node.input.kind !== 'continuation' || !node.input.replyId || !node.parent)
6538
+ return unavailable();
6539
+ const source = this.deliveryStore.workItemReplies.inspectHistoricalSource({ id: node.input.replyId, expectedSourceDigest: node.input.sourceDigest });
6540
+ const inbox = source === undefined ? undefined : this.deliveryStore.getInbox(source.id), binding = source === undefined ? undefined : this.deliveryStore.getBinding(source.binding.id);
6541
+ const body = material.continuationInputs[index - 1];
6542
+ if (!source || !inbox || !binding || !source.child || source.child.itemId !== node.itemId || source.child.planDigest !== node.planDigest
6543
+ || source.executionParentItemId !== node.parent.itemId || source.inputSha256 !== node.input.inputSha256 || source.inputBytes !== node.input.inputBytes
6544
+ || source.inputText !== body.text || inbox.envelope.kind !== 'text' || !inbox.envelope.text.trim() || (inbox.envelope.attachments?.length ?? 0) > 0
6545
+ || source.owner.principalRecordId !== owner.principalRecordId || source.owner.principalVersion !== owner.principalVersion || source.principalId !== input.principalId
6546
+ || source.scope.workspace !== input.workspace || source.scope.preset !== input.agentPreset || source.binding.generation > owner.generation
6547
+ || source.binding.generation < previous.generation || (source.binding.generation === previous.generation && (source.binding.id !== previous.id || source.binding.version !== previous.version))
6548
+ || binding.generation !== source.binding.generation || !(binding.status === 'active' && binding.version === source.binding.version || binding.status === 'revoked' && binding.version === source.binding.version + 1)
6549
+ || externalPrincipalId(binding.principal) !== input.principalId || binding.workspace !== input.workspace || binding.agentPreset !== input.agentPreset
6550
+ || workItemReplyCanonical(inbox.envelope.principal) !== workItemReplyCanonical(binding.principal) || workItemReplyCanonical(inbox.envelope.conversation) !== workItemReplyCanonical(binding.conversation))
6551
+ return unavailable();
6552
+ previous = source.binding;
6553
+ }
6554
+ return text;
6555
+ };
6556
+ const initial = frameWorkItemEngineeringContext(material, ownerInputs());
6557
+ const scope = evaluation.canonicalHostScope({ workspace: input.workspace, preset: input.agentPreset });
6558
+ const { scopeWatermark } = evaluation.listTrustedTaskLearningProjections({ scope, limit: 1 });
6559
+ const result = evaluation.withTrustedCanonicalScopeWriterFence({ scope, scopeWatermark }, () => peer.withOwnerWorkItemEngineeringTargetsFence([capability], materials => {
6560
+ if (!Array.isArray(materials) || materials.length !== 1)
6561
+ return unavailable();
6562
+ const currentMaterial = validateWorkItemEngineeringMaterial(materials[0]);
6563
+ if (workItemReplyCanonical(currentMaterial) !== workItemReplyCanonical(material))
6564
+ return unavailable();
6565
+ // No owns, issue, resolve or other opaque producer callbacks below this point.
6566
+ const owner = this.validateOwnerRoute(input);
6567
+ if (!this.active || owner.authorityHash !== input.expectedOwner.authorityHash || owner.principalRecordId !== input.expectedOwner.principalRecordId
6568
+ || owner.principalVersion !== input.expectedOwner.principalVersion || workItemPeerIdentity(this.context.get('assistantWorkItems', false)) !== peerIdentity
6569
+ || workItemPeerIdentity(this.context.get('assistantEvaluation', false)) !== evaluationIdentity)
6570
+ return unavailable();
6571
+ this.workItemFeedbackFence(state.intent, true, state.resolved.registration);
6572
+ const canonical = evaluation.getTrustedWorkItemLearningProjection({ scope, itemId: input.itemId });
6573
+ const currentRevision = evaluation.inspectTrustedTaskOwnerRevision({ scope, outcomeId: input.outcomeId, principalRecordId: owner.principalRecordId, principalVersion: owner.principalVersion });
6574
+ if (!canonical?.objective || canonical.projection.subjectKind !== 'work-item' || canonical.projection.subjectRef !== input.itemId
6575
+ || canonical.objective.outcomeId !== input.outcomeId || workItemReplyCanonical(canonical.projection) !== workItemReplyCanonical(task.canonical.projection)
6576
+ || workItemReplyCanonical(canonical.objective) !== workItemReplyCanonical(task.canonical.objective)
6577
+ || workItemReplyCanonical(canonical.execution) !== workItemReplyCanonical(task.canonical.execution)
6578
+ || workItemReplyCanonical(currentRevision) !== workItemReplyCanonical(revision))
6579
+ return unavailable();
6580
+ const frame = frameWorkItemEngineeringContext(currentMaterial, ownerInputs());
6581
+ if (frame.manifest.manifestDigest !== initial.manifest.manifestDigest || currentMaterial.feedbackProofDigest !== proof.proofDigest)
6582
+ return unavailable();
6583
+ if (expected && (task.sourceDigest !== expected.expectedSourceDigest || task.presentationDigest !== expected.expectedContentDigest
6584
+ || canonical.projection.version !== expected.canonical.version || canonical.projection.digest !== expected.canonical.digest
6585
+ || canonical.objective.status !== expected.canonical.objectiveStatus || frame.manifest.manifestDigest !== expected.expectedManifestDigest
6586
+ || frame.manifest.objective.fullTextDigest !== expected.expectedObjectiveDigest))
6587
+ return unavailable();
6588
+ return callback(Object.freeze({ task: Object.freeze({ ...task, owner, canonical }), manifest: frame.manifest, objective: frame.objective, material: currentMaterial }));
6589
+ }));
6590
+ if (!result.matched)
6591
+ return unavailable();
6592
+ return result.value;
6593
+ });
6594
+ }
4562
6595
  inspectOwnerForegroundLearningTask(input) {
4563
6596
  this.assertActive();
4564
6597
  const routeInput = { authorityId: input.authorityId, principalId: input.principalId,
@@ -4695,18 +6728,23 @@ export class AssistantDeliveryService extends Service {
4695
6728
  }
4696
6729
  const resolved = this.resolveOwnerRoute(input.authorityId);
4697
6730
  const authority = this.ownerRoutes.get(resolved.authorityId);
6731
+ const prepare = () => this.prepareAttentionDelivery({ lane: 'background-route', envelope: input.attention, sourceId,
6732
+ binding: () => this.deliveryStore.getActiveBinding(authority.conversation),
6733
+ idempotencyKey, text: input.text, format: input.format ?? 'plain' });
6734
+ // Stage 3: reject before Policy writes its allow row or spends budget (see enqueueBackgroundAuthorized).
6735
+ const early = this.config.attentionEnforce === true ? prepare() : undefined;
4698
6736
  const decision = authorizeOwnerRoute(this.policy, authority, sourceId, idempotencyKey);
4699
6737
  if (decision.effect !== 'allow')
4700
6738
  throw policyDenied(decision);
6739
+ const delivered = early ?? prepare();
4701
6740
  try {
4702
- return this.deliveryStore.enqueueOwnerRoute({
6741
+ return this.auditAttention('background-route', sourceId, { envelope: input.attention }, this.deliveryStore.enqueueOwnerRoute({
4703
6742
  authority,
4704
6743
  sourceId,
4705
6744
  sourceHash: createHash('sha256').update(sourceId).digest('hex'),
4706
6745
  idempotencyKey,
4707
- text: input.text,
4708
- format: input.format ?? 'plain',
4709
- });
6746
+ ...delivered,
6747
+ }));
4710
6748
  }
4711
6749
  catch (error) {
4712
6750
  if (error instanceof DeliveryStoreError && error.code === 'invalid-binding') {
@@ -5197,7 +7235,9 @@ export class AssistantDeliveryService extends Service {
5197
7235
  }
5198
7236
  }
5199
7237
  async tick() {
5200
- this.assertActive();
7238
+ // Timer-driven entry: a tick that fires after dispose is a no-op, not an error.
7239
+ if (!this.active)
7240
+ return;
5201
7241
  await this.drainInboundAdmissions();
5202
7242
  this.drainRestartContinuations();
5203
7243
  const restartFence = this.deliveryStore.getRestartFence();
@@ -5218,6 +7258,7 @@ export class AssistantDeliveryService extends Service {
5218
7258
  await this.drainDeliveryPresentations();
5219
7259
  await this.drainPreferenceProjections();
5220
7260
  await this.drainNaturalObjectiveIntents();
7261
+ await this.drainWorkItemFeedback();
5221
7262
  await this.drainWorkflowTraces();
5222
7263
  }
5223
7264
  async whenIdle() {
@@ -5229,6 +7270,7 @@ export class AssistantDeliveryService extends Service {
5229
7270
  this.presentationFlight,
5230
7271
  this.preferenceProjectionFlight,
5231
7272
  this.naturalObjectiveFlight,
7273
+ this.workItemFeedbackFlight,
5232
7274
  this.workflowTraceFlight,
5233
7275
  this.inboundAdmissionRecoveryFlight,
5234
7276
  this.interruptedInboxNoticeFlight,
@@ -5434,6 +7476,13 @@ export class AssistantDeliveryService extends Service {
5434
7476
  if (binding === undefined)
5435
7477
  continue;
5436
7478
  if (!reset && !stop) {
7479
+ // A pending owner-question reply is finished (or held for a retry)
7480
+ // before it can be queued to the Agent.
7481
+ const attentionReply = this.inspectOwnerQuestionReply(pending, binding);
7482
+ if (attentionReply === 'handled')
7483
+ continue;
7484
+ if (attentionReply === 'transient')
7485
+ return;
5437
7486
  this.deliveryStore.queueInbox(pending.id, binding.id);
5438
7487
  continue;
5439
7488
  }
@@ -5505,7 +7554,7 @@ export class AssistantDeliveryService extends Service {
5505
7554
  this.bindingFlights.set(key, promise);
5506
7555
  return promise;
5507
7556
  }
5508
- async stopInternal() {
7557
+ clearSchedulerTimers() {
5509
7558
  if (this.timer !== undefined)
5510
7559
  clearInterval(this.timer);
5511
7560
  if (this.modelSelectionRetryTimer !== undefined)
@@ -5518,6 +7567,9 @@ export class AssistantDeliveryService extends Service {
5518
7567
  this.modelSelectionRetryTimer = undefined;
5519
7568
  this.workflowTraceRetryTimer = undefined;
5520
7569
  this.preferenceProjectionRetryTimer = undefined;
7570
+ }
7571
+ async stopInternal() {
7572
+ this.clearSchedulerTimers();
5521
7573
  this.cancelModelSelectionWaiters();
5522
7574
  this.inboundAdmissionRecoveryController.abort(new Error('assistant-delivery is stopping'));
5523
7575
  this.restartController.abort(new Error('assistant-delivery is stopping'));
@@ -5538,6 +7590,7 @@ export class AssistantDeliveryService extends Service {
5538
7590
  this.presentationFlight,
5539
7591
  this.preferenceProjectionFlight,
5540
7592
  this.naturalObjectiveFlight,
7593
+ this.workItemFeedbackFlight,
5541
7594
  this.workflowTraceFlight,
5542
7595
  this.restartFlight,
5543
7596
  ]);