@dsh-enhanced/assistant-delivery 0.1.66 → 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.
package/lib/service.js CHANGED
@@ -33,7 +33,7 @@ import { bindingMatchesOwnerRoute, canonicalBackgroundSourceId as canonicalHostB
33
33
  import { isExactDeliveryCommand, parseDeliveryCommand } from './session-commands.js';
34
34
  import { deliveryT1PreferenceKeys, deliveryT1PreferenceValues } from './learning-command.js';
35
35
  import { assertReplyReplayAllowed, blockAgentRepliesForReplay as createReplyReplayBlock, } from './reply-replay.js';
36
- import { feedbackSignalInput, parseFeedbackCommand, parseNaturalObjectiveFeedback, objectiveStateMessage, classifyNaturalPreferenceDirective, observedResponseLanguage, } from './feedback-command.js';
36
+ import { feedbackSignalInput, parseFeedbackCommand, parseNaturalObjectiveFeedback, parseNaturalOwnerReply, objectiveStateMessage, classifyNaturalPreferenceDirective, observedResponseLanguage, } from './feedback-command.js';
37
37
  import { DELIVERY_PREFERENCE_PROJECTION_PROTOCOL, TRUSTED_DELIVERY_PRESENTATION_PRODUCER_PROTOCOL, } from './types.js';
38
38
  const trustedDeliveryPreferenceProducers = new WeakSet();
39
39
  const deliveryPreferenceProducerProbe = Symbol('assistant-delivery.preference-producer-probe');
@@ -265,6 +265,7 @@ function containsReservedLearningMetadata(metadata) {
265
265
  return metadata !== undefined && Object.keys(metadata).some(key => key.startsWith(learningMetadataPrefix));
266
266
  }
267
267
  export const ownerNotificationWithdrawalProtocol = 'assistant-delivery/owner-notification-withdrawal/v1';
268
+ export const ownerNotificationReplyProtocol = 'assistant-delivery/owner-notification-reply/v1';
268
269
  export class AssistantDeliveryError extends Error {
269
270
  code;
270
271
  constructor(code, message) {
@@ -338,6 +339,16 @@ const configSchema = Schema.object({
338
339
  attentionAttachOverCap: Schema.boolean().default(false),
339
340
  attentionEnforce: Schema.boolean().default(false),
340
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([]),
341
352
  ownerRoutes: Schema.array(Schema.object({
342
353
  id: Schema.string().min(1).required(),
343
354
  conversation: Schema.object({
@@ -696,6 +707,8 @@ export class AssistantDeliveryService extends Service {
696
707
  ownerNotificationWithdrawals = new Map();
697
708
  /** Live handles issued by `registerOwnerNotificationWithdrawal`, mapped to their (authority, source) key. */
698
709
  ownerNotificationWithdrawalHandles = new Map();
710
+ /** Slice-5 reply consumers, one per (authority, canonical owner-notice source id). */
711
+ ownerNotificationReplies = new Map();
699
712
  foregroundObserver;
700
713
  /** Once a live verifier required acceptance, loss of that verifier must not silently bypass it. */
701
714
  acceptanceRequired = false;
@@ -2817,6 +2830,16 @@ export class AssistantDeliveryService extends Service {
2817
2830
  return inbox !== undefined && binding !== undefined && inbox.bindingId === binding.id
2818
2831
  && this.isInboundAuthorized(binding, inbox.envelope);
2819
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
+ }
2820
2843
  isInboundAuthorized(binding, envelope) {
2821
2844
  if (!this.active)
2822
2845
  return false;
@@ -3251,6 +3274,25 @@ export class AssistantDeliveryService extends Service {
3251
3274
  void this.drainWorkflowTraces();
3252
3275
  return 'recorded';
3253
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
+ }
3254
3296
  /**
3255
3297
  * Convert only an authenticated reply to one exact Automation delivery into
3256
3298
  * a trusted objective label. The original run timestamp is retained because
@@ -3259,7 +3301,7 @@ export class AssistantDeliveryService extends Service {
3259
3301
  */
3260
3302
  async dispatchNaturalObjectiveFeedback(binding, envelope) {
3261
3303
  const selection = envelope.kind === 'text' && (envelope.attachments?.length ?? 0) === 0
3262
- ? parseNaturalObjectiveFeedback(envelope.text) : undefined;
3304
+ ? this.naturalObjectiveSelection(envelope.text) : undefined;
3263
3305
  if (selection === undefined || !this.isOwnerFeedbackController(binding, envelope))
3264
3306
  return;
3265
3307
  const replyTarget = envelope.metadata?.replyToProviderMessageId;
@@ -3375,7 +3417,7 @@ export class AssistantDeliveryService extends Service {
3375
3417
  const naturalMatches = naturalIntent !== undefined && inbox.envelope.kind === 'text'
3376
3418
  && naturalIntent.envelopeHash === inbox.envelopeHash
3377
3419
  && naturalIntent.sourceOutboxId === target.id
3378
- && parseNaturalObjectiveFeedback(inbox.envelope.text) === naturalIntent.selection
3420
+ && this.naturalObjectiveSelection(inbox.envelope.text) === naturalIntent.selection
3379
3421
  && (parsed.kind === 'objective-status' || (naturalIntent.status === 'frozen'
3380
3422
  && JSON.stringify(naturalIntent.command) === JSON.stringify(parsed)));
3381
3423
  if ((inbox.envelope.attachments?.length ?? 0) !== 0 || (!typedMatches && !naturalMatches))
@@ -3803,7 +3845,7 @@ export class AssistantDeliveryService extends Service {
3803
3845
  const sessions = ctx.get('sessions');
3804
3846
  const agent = request.agent;
3805
3847
  const events = agent.session.snapshotEvents();
3806
- const reviewer = approvalReviewerOf(events);
3848
+ const reviewer = approvalReviewerOf(events, agent.session.header.version);
3807
3849
  const reviewRoute = reviewer === 'user'
3808
3850
  ? 'user'
3809
3851
  : reviewer === 'auto-review' && ctx.assistantPolicy.isAutoReviewEscalation(request)
@@ -3942,7 +3984,8 @@ export class AssistantDeliveryService extends Service {
3942
3984
  return next();
3943
3985
  let reviewer;
3944
3986
  try {
3945
- reviewer = approvalReviewerOf(request.agent.session.snapshotEvents());
3987
+ const session = request.agent.session;
3988
+ reviewer = approvalReviewerOf(session.snapshotEvents(), session.header.version);
3946
3989
  }
3947
3990
  catch {
3948
3991
  return 'unavailable';
@@ -4121,6 +4164,32 @@ export class AssistantDeliveryService extends Service {
4121
4164
  if (!['received', 'authorized'].includes(record.status)) {
4122
4165
  return { duplicate: accepted.duplicate, inboxId: record.id, status: record.status };
4123
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
+ }
4124
4193
  if (!this.deliveryStore.isAuthorizedPrincipal(envelope.principal)) {
4125
4194
  record = this.deliveryStore.deadLetterInbox(record.id, 'unauthorized-principal');
4126
4195
  return { duplicate: accepted.duplicate, inboxId: record.id, status: record.status };
@@ -4225,50 +4294,114 @@ export class AssistantDeliveryService extends Service {
4225
4294
  const failed = this.deliveryStore.getInbox(record.id);
4226
4295
  return { duplicate: accepted.duplicate, inboxId: failed.id, status: failed.status };
4227
4296
  }
4228
- const feedback = this.admitOwnerWorkItemFeedback(record, binding);
4229
- if (feedback === 'handled') {
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();
4230
4333
  void this.drainWorkItemFeedback().catch(() => { });
4231
4334
  return { duplicate: accepted.duplicate, inboxId: record.id, status: 'processed' };
4232
4335
  }
4233
- if (feedback !== 'ordinary' && this.admitWorkItemReply(record, binding)) {
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)) {
4234
4341
  const handedOff = this.deliveryStore.getInbox(record.id);
4235
4342
  this.notifyWorkItemReplyPending();
4343
+ if (feedback.kind === 'ordinary')
4344
+ this.recordPartialMixedFeedbackAdmission(record, 'feedback-not-admitted');
4236
4345
  return { duplicate: accepted.duplicate, inboxId: handedOff.id, status: handedOff.status };
4237
4346
  }
4347
+ if (feedback.kind === 'ordinary' && splitContinuation)
4348
+ this.recordPartialMixedFeedbackAdmission(record, 'neither-lane-admitted');
4238
4349
  record = this.deliveryStore.queueInbox(record.id, binding.id);
4239
4350
  return { duplicate: accepted.duplicate, inboxId: record.id, status: record.status };
4240
4351
  });
4241
4352
  }
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
+ */
4242
4359
  parseWorkItemFeedback(envelope) {
4243
4360
  if ((envelope.attachments?.length ?? 0) !== 0)
4244
4361
  return undefined;
4245
4362
  if (envelope.kind === 'text') {
4246
4363
  const text = envelope.text.trim();
4247
- if (!/^(?:问题解决了|已经解决了|现在解决了|还是不行|还没解决|问题还没解决|撤回刚才的任务反馈|撤回我刚才的任务反馈)[。.!!]?$/u.test(text))
4248
- return undefined;
4249
- const value = parseNaturalObjectiveFeedback(text);
4250
- return value === undefined ? undefined : { kind: 'natural', value };
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
+ };
4251
4375
  }
4252
4376
  const command = parseDeliveryCommand(envelope);
4253
4377
  if (command?.name !== 'feedback')
4254
4378
  return undefined;
4255
4379
  const parsed = parseFeedbackCommand(command.rawInput);
4256
4380
  return ['objective', 'objective-status', 'objective-revision'].includes(parsed.kind)
4257
- ? { kind: 'explicit', command: parsed } : undefined;
4381
+ ? { selection: { kind: 'explicit', command: parsed }, continuation: false }
4382
+ : undefined;
4258
4383
  }
4259
4384
  workItemFeedbackPermission(binding, registration) {
4260
4385
  return this.policy.evaluate({ subject: { kind: 'external', id: externalPrincipalId(binding.principal), workspace: binding.workspace, principal: externalPrincipalId(binding.principal) },
4261
4386
  action: 'feedback-work-items', resource: { kind: 'automation', id: registration.continuationAutomationId }, context: { initiator: 'external' } }).effect === 'allow';
4262
4387
  }
4263
- admitOwnerWorkItemFeedback(inbox, binding) {
4264
- const registration = this.currentWorkItemReplyConsumer(), selection = this.parseWorkItemFeedback(inbox.envelope);
4265
- if (!registration || registration.feedbackEnabled !== true || !selection || !inbox.envelope.metadata?.replyToProviderMessageId)
4266
- return 'none';
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' };
4267
4400
  const report = this.deliveryStore.workItemReplies.replyReport(inbox.channel, inbox.account, inbox.envelope.metadata.replyToProviderMessageId);
4268
4401
  if (!report || !['accepted', 'delivered', 'read'].includes(report.outbox.status) || !report.outbox.providerMessageId
4269
4402
  || workItemReplyCanonical(report.outbox.intent.target.conversation) !== workItemReplyCanonical(inbox.envelope.conversation)
4270
4403
  || workItemReplyCanonical(report.outbox.intent.target.principal) !== workItemReplyCanonical(binding.principal))
4271
- return 'none';
4404
+ return { kind: 'none' };
4272
4405
  let target;
4273
4406
  try {
4274
4407
  target = this.inspectWorkItemReport({ sourceId: registration.sourceId, itemId: report.target.itemId, reportRevision: report.target.reportRevision,
@@ -4276,25 +4409,30 @@ export class AssistantDeliveryService extends Service {
4276
4409
  format: report.outbox.intent.format === 'markdown' ? 'markdown' : 'plain' }, true);
4277
4410
  }
4278
4411
  catch {
4279
- return 'none';
4412
+ return { kind: 'none' };
4280
4413
  }
4281
4414
  if (workItemReplyCanonical(target) !== workItemReplyCanonical(report.target))
4282
- return 'none';
4415
+ return { kind: 'none' };
4416
+ const continuation = parsed.continuation;
4283
4417
  // A recognized result must not be converted into execution when its separate feedback permission is absent.
4284
4418
  if (!this.workItemFeedbackPermission(binding, registration))
4285
- return 'ordinary';
4419
+ return { kind: 'ordinary', continuation };
4286
4420
  const source = { protocol: 'assistant-delivery/work-item-feedback/v1', inboxId: inbox.id, envelopeHash: inbox.envelopeHash,
4287
4421
  inputText: inbox.envelope.text, inputSha256: createHash('sha256').update(inbox.envelope.text).digest('hex'), admissionCursor: inbox.admissionCursor, occurredAt: inbox.receivedAt,
4288
4422
  owner: target.owner, principalId: externalPrincipalId(binding.principal), binding: { id: binding.id, version: binding.version, generation: binding.generation }, scope: target.scope,
4289
- target: { outboxId: report.outbox.id, providerMessageId: report.outbox.providerMessageId, itemId: target.itemId, reportRevision: target.reportRevision, reportDigest: target.reportDigest, streamId: target.streamId }, selection,
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,
4290
4424
  operationId: `owner-work-item-feedback:${createHash('sha256').update(JSON.stringify([inbox.id, inbox.envelopeHash])).digest('hex')}` };
4291
- this.deliveryStore.workItemFeedback.admit(source, () => {
4425
+ const fence = () => {
4292
4426
  const current = this.currentWorkItemReplyConsumer(), owner = this.ownerLineageForBinding(binding), active = this.deliveryStore.getBinding(binding.id);
4293
4427
  if (current !== registration || current?.feedbackEnabled !== true || workItemReplyCanonical(owner) !== workItemReplyCanonical(source.owner)
4294
4428
  || workItemReplyCanonical(active) !== workItemReplyCanonical(binding) || !this.workItemFeedbackPermission(binding, registration))
4295
4429
  throw new Error('feedback admission authority changed');
4296
- });
4297
- return 'handled';
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})`);
4298
4436
  }
4299
4437
  /** Passing an explicit registration makes this the callback-free final local authority check. */
4300
4438
  workItemFeedbackFence(intent, recorded = false, expectedRegistration) {
@@ -4596,7 +4734,11 @@ export class AssistantDeliveryService extends Service {
4596
4734
  || !/^[a-z][a-z0-9-]{0,54}$/u.test(principal.channel))
4597
4735
  return undefined;
4598
4736
  const key = { channel: principal.channel, account: principal.account, tenant: principal.tenant, user: principal.user };
4599
- const senderAllowlisted = this.deliveryStore.isAuthorizedPrincipal(key);
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';
4600
4742
  // A follow-up counts only from an allowlisted sender replying, in the same
4601
4743
  // conversation, to a delivered WorkItems report.
4602
4744
  let threadFollowUp = false;
@@ -4879,32 +5021,43 @@ export class AssistantDeliveryService extends Service {
4879
5021
  action: 'continue-work-items-from-reply', resource: { kind: 'automation', id: registration.continuationAutomationId },
4880
5022
  context: { initiator: 'external' } }).effect === 'allow';
4881
5023
  }
4882
- admitWorkItemReply(inbox, binding) {
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) {
4883
5030
  const registration = this.currentWorkItemReplyConsumer();
4884
5031
  const envelope = inbox.envelope;
4885
5032
  if (!registration || registration.continuationEnabled === false || envelope.kind !== 'text' || envelope.text.trim().startsWith('/') || !envelope.text
4886
5033
  || (envelope.attachments?.length ?? 0) > 0 || !envelope.metadata?.replyToProviderMessageId)
4887
- return false;
5034
+ return undefined;
4888
5035
  const report = this.deliveryStore.workItemReplies.replyReport(envelope.channel, envelope.account, envelope.metadata.replyToProviderMessageId);
4889
5036
  const outbox = report?.outbox;
4890
5037
  if (!report || !outbox || !['accepted', 'delivered', 'read'].includes(outbox.status) || workItemReplyCanonical(outbox.intent.target.principal) !== workItemReplyCanonical(binding.principal)
4891
5038
  || workItemReplyCanonical(outbox.intent.target.conversation) !== workItemReplyCanonical(envelope.conversation))
4892
- return false;
5039
+ return undefined;
4893
5040
  try {
4894
5041
  const target = this.inspectWorkItemReport({ sourceId: registration.sourceId, itemId: report.target.itemId,
4895
5042
  reportRevision: report.target.reportRevision, bindingId: outbox.intent.bindingId, idempotencyKey: outbox.intent.idempotencyKey,
4896
5043
  text: outbox.intent.text, format: outbox.intent.format === 'markdown' ? 'markdown' : 'plain' }, true);
4897
5044
  if (workItemReplyCanonical(target) !== workItemReplyCanonical(report.target))
4898
- return false;
5045
+ return undefined;
4899
5046
  if (registration.canContinueReport?.({ itemId: target.itemId, reportRevision: target.reportRevision, bindingId: outbox.intent.bindingId,
4900
5047
  idempotencyKey: outbox.intent.idempotencyKey, text: outbox.intent.text, format: outbox.intent.format === 'markdown' ? 'markdown' : 'plain' }) === false)
4901
- return false;
5048
+ return undefined;
4902
5049
  if (this.currentWorkItemReplyConsumer() !== registration || !this.workItemReplyPermission(binding, registration))
4903
- return false;
5050
+ return undefined;
4904
5051
  }
4905
5052
  catch {
4906
- return false;
5053
+ return undefined;
4907
5054
  }
5055
+ return report;
5056
+ }
5057
+ admitWorkItemReply(inbox, binding) {
5058
+ const report = this.prepareWorkItemReply(inbox, binding);
5059
+ if (report === undefined)
5060
+ return false;
4908
5061
  // From here a failed durable handoff is an error, never ordinary fallback.
4909
5062
  this.deliveryStore.workItemReplies.admit(inbox.id, binding, report);
4910
5063
  return true;
@@ -5496,6 +5649,148 @@ export class AssistantDeliveryService extends Service {
5496
5649
  return undefined;
5497
5650
  return Object.freeze({ outboxId: record.id, status: record.status });
5498
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
+ }
5499
5794
  /**
5500
5795
  * Dedicated Automation result lane. Every learning field is derived from an
5501
5796
  * authoritative Automations receipt; callers cannot supply metadata,
@@ -7181,6 +7476,13 @@ export class AssistantDeliveryService extends Service {
7181
7476
  if (binding === undefined)
7182
7477
  continue;
7183
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;
7184
7486
  this.deliveryStore.queueInbox(pending.id, binding.id);
7185
7487
  continue;
7186
7488
  }