@dsh-enhanced/assistant-delivery 0.1.66 → 0.1.68

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({
@@ -435,6 +446,23 @@ const ownerRequestedChangeArgumentBytes = 64 * 1024;
435
446
  const toolApprovalIdentityBytes = 512;
436
447
  /** Bounded owner-ordered linked fallback list for one report route. */
437
448
  const maxLinkedReportFallback = 8;
449
+ /**
450
+ * Host-native tool arguments normalized exactly as Delivery attests them:
451
+ * JSON.parse then JSON.stringify, preserving key order and rejecting any
452
+ * non-object payload. Other components must hash this exact text instead of
453
+ * re-canonicalizing it, otherwise a foreground witness cannot be matched.
454
+ */
455
+ export function normalizeForegroundToolArguments(raw) {
456
+ try {
457
+ const parsed = JSON.parse(raw);
458
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
459
+ return undefined;
460
+ return JSON.stringify(parsed);
461
+ }
462
+ catch {
463
+ return undefined;
464
+ }
465
+ }
438
466
  function inspectUnsettledNativeCall(events, turn, callId, toolName, argumentsJson, argumentBytes = toolApprovalArgumentBytes) {
439
467
  if (!callId || !toolName || Buffer.byteLength(callId) > toolApprovalIdentityBytes
440
468
  || Buffer.byteLength(toolName) > toolApprovalIdentityBytes
@@ -463,16 +491,9 @@ function inspectUnsettledNativeCall(events, turn, callId, toolName, argumentsJso
463
491
  || events.filter(event => event.type === 'step/start' && event.data.turn === turn
464
492
  && event.data.step === call.data.step).length !== 1)
465
493
  return undefined;
466
- let normalizedArguments;
467
- try {
468
- const parsed = JSON.parse(call.data.arguments);
469
- if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
470
- return undefined;
471
- normalizedArguments = JSON.stringify(parsed);
472
- }
473
- catch {
494
+ const normalizedArguments = normalizeForegroundToolArguments(call.data.arguments);
495
+ if (normalizedArguments === undefined)
474
496
  return undefined;
475
- }
476
497
  const argumentsDigest = createHash('sha256').update(normalizedArguments).digest('hex');
477
498
  if (argumentsDigest !== createHash('sha256').update(argumentsJson).digest('hex'))
478
499
  return undefined;
@@ -696,6 +717,8 @@ export class AssistantDeliveryService extends Service {
696
717
  ownerNotificationWithdrawals = new Map();
697
718
  /** Live handles issued by `registerOwnerNotificationWithdrawal`, mapped to their (authority, source) key. */
698
719
  ownerNotificationWithdrawalHandles = new Map();
720
+ /** Slice-5 reply consumers, one per (authority, canonical owner-notice source id). */
721
+ ownerNotificationReplies = new Map();
699
722
  foregroundObserver;
700
723
  /** Once a live verifier required acceptance, loss of that verifier must not silently bypass it. */
701
724
  acceptanceRequired = false;
@@ -2091,10 +2114,53 @@ export class AssistantDeliveryService extends Service {
2091
2114
  completeExecution: (inboxId, input) => this.completeForegroundTaskExecution(inboxId, input),
2092
2115
  recordExecutionModelSelection: (inboxId, input) => this.deliveryStore.recordForegroundExecutionModelSelection({ inboxId, ...input }),
2093
2116
  claimed: (agent, envelope, turn) => this.capturePreferenceTurn(agent, { kind: 'delivery', channel: envelope.channel, account: envelope.account, eventId: envelope.eventId }, turn),
2094
- notifications: binding => this.deliveryStore.listOutbox({ bindingId: binding.id, limit: 50 }).filter(record => record.status === 'accepted'
2095
- // Already-accepted notices stay listed: withdrawal only gates the send attempt.
2117
+ notifications: binding => this.deliveryStore.listOutbox({ bindingId: binding.id, limit: 50 }).filter(record => (record.status === 'accepted' || record.status === 'delivered')
2118
+ // Acknowledged notices stay listed until read; withdrawal only gates the send attempt.
2096
2119
  && record.intent.metadata?.['dsh.native-notice'] === 'v1' && this.#notificationGuard(record, { withdrawal: false }) === undefined)
2097
- .map(record => ({ id: record.id, text: record.intent.text, createdAt: record.createdAt })),
2120
+ .map(record => ({ id: record.id, text: record.intent.text, createdAt: record.createdAt,
2121
+ status: record.status === 'delivered' ? 'delivered' : 'accepted' })),
2122
+ acknowledgeNotices: (binding, ids, status) => {
2123
+ // Validate the whole batch against this binding and the exact send-path
2124
+ // guard before writing anything: one foreign/forged/expired id, drifted
2125
+ // owner/binding/route/policy or withdrawn notice rejects the batch with
2126
+ // zero receipts.
2127
+ const byId = new Map(this.deliveryStore.listOutbox({ bindingId: binding.id, limit: 100 }).map(record => [record.id, record]));
2128
+ const targets = ids.map(id => {
2129
+ const record = byId.get(id);
2130
+ if (record === undefined || record.intent.metadata?.['dsh.native-notice'] !== 'v1'
2131
+ || (record.status !== 'accepted' && record.status !== 'delivered' && record.status !== 'read')
2132
+ || record.providerMessageId === undefined) {
2133
+ throw new Error('assistant-delivery: native notice acknowledgement is invalid');
2134
+ }
2135
+ // Re-run the exact send-path guard: expiry, owner/binding version or
2136
+ // generation drift, route/policy change and withdrawal all fail closed
2137
+ // before any receipt is written. withdrawal:true is required so a
2138
+ // withdrawn notice cannot be acknowledged after it was sent.
2139
+ const refused = this.#nativeNoticeGuard(record.intent, { withdrawal: true });
2140
+ if (refused !== undefined)
2141
+ throw new Error(`assistant-delivery: native notice acknowledgement is ${refused.failureCode}`);
2142
+ return record;
2143
+ });
2144
+ const rank = { accepted: 1, delivered: 2, read: 3 };
2145
+ for (const record of targets) {
2146
+ // A late lower-rank ack is a no-op: it must not append a stale receipt
2147
+ // after read/delivered nor downgrade the outbox state.
2148
+ if (rank[status] <= rank[record.status])
2149
+ continue;
2150
+ try {
2151
+ this.deliveryStore.recordReceipt({ channel: binding.conversation.channel, account: binding.conversation.account,
2152
+ providerMessageId: record.providerMessageId, status, occurredAt: Date.now() });
2153
+ }
2154
+ catch (error) {
2155
+ // The web client re-acks every rendered notice. A repeat with a new
2156
+ // timestamp is an idempotent no-op, not a client error; other
2157
+ // storage failures still propagate.
2158
+ if (!(error instanceof DeliveryStoreError) || error.code !== 'idempotency-conflict')
2159
+ throw error;
2160
+ }
2161
+ }
2162
+ return Object.freeze({ acknowledged: targets.length });
2163
+ },
2098
2164
  }, config);
2099
2165
  this.nativeWebOwner = access;
2100
2166
  this.nativeWebBound = true;
@@ -2185,6 +2251,55 @@ export class AssistantDeliveryService extends Service {
2185
2251
  return Object.freeze({ protocol: 'assistant-delivery/foreground-tool-input/v1',
2186
2252
  attestation, ...page });
2187
2253
  }
2254
+ /**
2255
+ * The live caller's unique configured owner route. A caller never names a
2256
+ * route here: zero matching routes (anonymous, unconfigured, or failed
2257
+ * revalidation) and more than one matching route (ambiguous owner authority)
2258
+ * both fail closed.
2259
+ */
2260
+ ownerRouteForAgent(agent) {
2261
+ if (!this.active || typeof agent !== 'object' || agent === null || agent.session === undefined)
2262
+ return undefined;
2263
+ let resolved;
2264
+ try {
2265
+ for (const authorityId of this.ownerRoutes.keys()) {
2266
+ if (this.validateOwnerAgentForRoute(agent, authorityId) === undefined)
2267
+ continue;
2268
+ if (resolved !== undefined)
2269
+ return undefined;
2270
+ resolved = authorityId;
2271
+ }
2272
+ }
2273
+ catch {
2274
+ return undefined;
2275
+ }
2276
+ return resolved;
2277
+ }
2278
+ /** Host-only proof resolved from the caller Agent's unique live owner route. */
2279
+ inspectOwnerForegroundToolCallForAgent(agent, input) {
2280
+ if (!input || typeof input !== 'object' || Reflect.ownKeys(input).some(key => typeof key !== 'string'
2281
+ || !['callId', 'toolName', 'argumentsJson'].includes(key)))
2282
+ return undefined;
2283
+ const authorityId = this.ownerRouteForAgent(agent);
2284
+ if (authorityId === undefined)
2285
+ return undefined;
2286
+ return this.inspectOwnerForegroundToolCall({ agent, authorityId, callId: input.callId,
2287
+ toolName: input.toolName, argumentsJson: input.argumentsJson });
2288
+ }
2289
+ /** Read-only human-input pages resolved from the caller Agent's unique live
2290
+ * owner route; the caller cannot select an Inbox, route, owner or replacement text. */
2291
+ readOwnerForegroundToolInputForAgent(agent, input) {
2292
+ if (!input || typeof input !== 'object' || Reflect.ownKeys(input).some(key => typeof key !== 'string'
2293
+ || !['callId', 'toolName', 'argumentsJson', 'offset', 'maxBytes'].includes(key)))
2294
+ return undefined;
2295
+ const authorityId = this.ownerRouteForAgent(agent);
2296
+ if (authorityId === undefined)
2297
+ return undefined;
2298
+ return this.readOwnerForegroundToolInput({ agent, authorityId, callId: input.callId,
2299
+ toolName: input.toolName, argumentsJson: input.argumentsJson,
2300
+ ...(input.offset === undefined ? {} : { offset: input.offset }),
2301
+ ...(input.maxBytes === undefined ? {} : { maxBytes: input.maxBytes }) });
2302
+ }
2188
2303
  /** Host-only, read-only source proof for an owner's live memory or wiki tool call. */
2189
2304
  inspectOwnerRequestedChange(execution, input) {
2190
2305
  if (!this.active || !execution || typeof execution !== 'object'
@@ -2817,6 +2932,16 @@ export class AssistantDeliveryService extends Service {
2817
2932
  return inbox !== undefined && binding !== undefined && inbox.bindingId === binding.id
2818
2933
  && this.isInboundAuthorized(binding, inbox.envelope);
2819
2934
  }
2935
+ /**
2936
+ * Exact owner-configured peer bot, or `undefined`. `senderIsBot` is only the
2937
+ * provider's declaration; the allowlisted tuple is the actual authority.
2938
+ */
2939
+ peerAgentAuthority(envelope) {
2940
+ if (envelope.metadata?.senderIsBot !== 'true')
2941
+ return undefined;
2942
+ return this.config.peerAgents?.find(peer => peer.channel === envelope.channel && peer.account === envelope.account
2943
+ && peer.tenant === envelope.principal.tenant && peer.user === envelope.principal.user);
2944
+ }
2820
2945
  isInboundAuthorized(binding, envelope) {
2821
2946
  if (!this.active)
2822
2947
  return false;
@@ -3251,6 +3376,25 @@ export class AssistantDeliveryService extends Service {
3251
3376
  void this.drainWorkflowTraces();
3252
3377
  return 'recorded';
3253
3378
  }
3379
+ /** Opt-in widened deterministic grammar; off keeps the legacy exact phrases. */
3380
+ widenedNaturalFeedbackCoverage() {
3381
+ return this.config.naturalFeedback?.coverage === true;
3382
+ }
3383
+ /** Splitting one reply into result + continuation requires both opt-ins. */
3384
+ mixedNaturalFeedbackIntent() {
3385
+ return this.config.naturalFeedback?.coverage === true
3386
+ && this.config.naturalFeedback?.mixedIntent === true;
3387
+ }
3388
+ /**
3389
+ * One canonical selection for admission and its later re-derivation fences.
3390
+ * The widened parser is a pure function, so a frozen intent keeps verifying
3391
+ * even after a restart that toggles the opt-in.
3392
+ */
3393
+ naturalObjectiveSelection(text) {
3394
+ return this.widenedNaturalFeedbackCoverage()
3395
+ ? parseNaturalOwnerReply(text)?.status
3396
+ : parseNaturalObjectiveFeedback(text);
3397
+ }
3254
3398
  /**
3255
3399
  * Convert only an authenticated reply to one exact Automation delivery into
3256
3400
  * a trusted objective label. The original run timestamp is retained because
@@ -3259,7 +3403,7 @@ export class AssistantDeliveryService extends Service {
3259
3403
  */
3260
3404
  async dispatchNaturalObjectiveFeedback(binding, envelope) {
3261
3405
  const selection = envelope.kind === 'text' && (envelope.attachments?.length ?? 0) === 0
3262
- ? parseNaturalObjectiveFeedback(envelope.text) : undefined;
3406
+ ? this.naturalObjectiveSelection(envelope.text) : undefined;
3263
3407
  if (selection === undefined || !this.isOwnerFeedbackController(binding, envelope))
3264
3408
  return;
3265
3409
  const replyTarget = envelope.metadata?.replyToProviderMessageId;
@@ -3375,7 +3519,7 @@ export class AssistantDeliveryService extends Service {
3375
3519
  const naturalMatches = naturalIntent !== undefined && inbox.envelope.kind === 'text'
3376
3520
  && naturalIntent.envelopeHash === inbox.envelopeHash
3377
3521
  && naturalIntent.sourceOutboxId === target.id
3378
- && parseNaturalObjectiveFeedback(inbox.envelope.text) === naturalIntent.selection
3522
+ && this.naturalObjectiveSelection(inbox.envelope.text) === naturalIntent.selection
3379
3523
  && (parsed.kind === 'objective-status' || (naturalIntent.status === 'frozen'
3380
3524
  && JSON.stringify(naturalIntent.command) === JSON.stringify(parsed)));
3381
3525
  if ((inbox.envelope.attachments?.length ?? 0) !== 0 || (!typedMatches && !naturalMatches))
@@ -3803,7 +3947,7 @@ export class AssistantDeliveryService extends Service {
3803
3947
  const sessions = ctx.get('sessions');
3804
3948
  const agent = request.agent;
3805
3949
  const events = agent.session.snapshotEvents();
3806
- const reviewer = approvalReviewerOf(events);
3950
+ const reviewer = approvalReviewerOf(events, agent.session.header.version);
3807
3951
  const reviewRoute = reviewer === 'user'
3808
3952
  ? 'user'
3809
3953
  : reviewer === 'auto-review' && ctx.assistantPolicy.isAutoReviewEscalation(request)
@@ -3942,7 +4086,8 @@ export class AssistantDeliveryService extends Service {
3942
4086
  return next();
3943
4087
  let reviewer;
3944
4088
  try {
3945
- reviewer = approvalReviewerOf(request.agent.session.snapshotEvents());
4089
+ const session = request.agent.session;
4090
+ reviewer = approvalReviewerOf(session.snapshotEvents(), session.header.version);
3946
4091
  }
3947
4092
  catch {
3948
4093
  return 'unavailable';
@@ -4121,6 +4266,32 @@ export class AssistantDeliveryService extends Service {
4121
4266
  if (!['received', 'authorized'].includes(record.status)) {
4122
4267
  return { duplicate: accepted.duplicate, inboxId: record.id, status: record.status };
4123
4268
  }
4269
+ // An explicitly allowlisted peer bot enters its own linked-principal lane.
4270
+ // It is still subject to the ordinary Policy ingest decision below and never
4271
+ // receives owner feedback/continuation behavior. The allowlist is re-read
4272
+ // for every provider-declared bot message: removing an entry revokes the
4273
+ // lane even while the old linked principal row is still active.
4274
+ const senderIsBot = envelope.metadata?.senderIsBot === 'true';
4275
+ const peerAuthority = senderIsBot ? this.peerAgentAuthority(envelope) : undefined;
4276
+ if (senderIsBot && peerAuthority === undefined) {
4277
+ record = this.deliveryStore.deadLetterInbox(record.id, 'unauthorized-principal');
4278
+ return { duplicate: accepted.duplicate, inboxId: record.id, status: record.status };
4279
+ }
4280
+ if (peerAuthority !== undefined && !this.deliveryStore.isAuthorizedPrincipal(envelope.principal)) {
4281
+ const owner = this.deliveryStore.activeOwnerPrincipal();
4282
+ if (owner === undefined) {
4283
+ record = this.deliveryStore.deadLetterInbox(record.id, 'peer-owner-unavailable');
4284
+ return { duplicate: accepted.duplicate, inboxId: record.id, status: record.status };
4285
+ }
4286
+ try {
4287
+ this.deliveryStore.ensurePeerPrincipal(envelope.principal, owner.id, { reauthorize: true });
4288
+ }
4289
+ catch (error) {
4290
+ this.context.logger.warn(`assistant-delivery: peer principal admission failed: ${String(error)}`);
4291
+ record = this.deliveryStore.deadLetterInbox(record.id, 'peer-principal-unavailable');
4292
+ return { duplicate: accepted.duplicate, inboxId: record.id, status: record.status };
4293
+ }
4294
+ }
4124
4295
  if (!this.deliveryStore.isAuthorizedPrincipal(envelope.principal)) {
4125
4296
  record = this.deliveryStore.deadLetterInbox(record.id, 'unauthorized-principal');
4126
4297
  return { duplicate: accepted.duplicate, inboxId: record.id, status: record.status };
@@ -4225,50 +4396,114 @@ export class AssistantDeliveryService extends Service {
4225
4396
  const failed = this.deliveryStore.getInbox(record.id);
4226
4397
  return { duplicate: accepted.duplicate, inboxId: failed.id, status: failed.status };
4227
4398
  }
4228
- const feedback = this.admitOwnerWorkItemFeedback(record, binding);
4229
- if (feedback === 'handled') {
4399
+ // Slice 5: a reply to the exact delivered owner-question notice is
4400
+ // consumed here, before any Agent admission. Applied replies are
4401
+ // processed; a transient consumer failure leaves the Inbox pending.
4402
+ const attentionReply = this.inspectOwnerQuestionReply(record, binding);
4403
+ if (attentionReply !== 'none') {
4404
+ const current = this.deliveryStore.getInbox(record.id);
4405
+ return { duplicate: accepted.duplicate, inboxId: current.id, status: current.status };
4406
+ }
4407
+ const feedback = this.inspectOwnerWorkItemFeedback(record, binding);
4408
+ const splitContinuation = feedback.kind !== 'none' && feedback.continuation;
4409
+ if (feedback.kind === 'ready') {
4410
+ // One reply may carry a result and an explicit continuation. They are two
4411
+ // independently fenced decisions: admit both atomically when both lanes
4412
+ // are permitted, otherwise record the result alone and record the reason.
4413
+ let admittedContinuation = false;
4414
+ if (splitContinuation) {
4415
+ const report = this.prepareWorkItemReply(record, binding);
4416
+ if (report !== undefined) {
4417
+ try {
4418
+ this.deliveryStore.admitOwnerWorkItemFeedbackWithReply({ feedback: feedback.source, binding, report, fence: feedback.fence });
4419
+ admittedContinuation = true;
4420
+ }
4421
+ catch (error) {
4422
+ // The combined transaction rolled back; fall back to the result lane
4423
+ // only. A failed result fence re-throws on that fallback, as before.
4424
+ this.context.logger.warn(`assistant-delivery: mixed owner feedback continuation admission failed: ${String(error)}`);
4425
+ }
4426
+ }
4427
+ }
4428
+ if (!admittedContinuation) {
4429
+ this.deliveryStore.workItemFeedback.admit(feedback.source, feedback.fence);
4430
+ if (splitContinuation)
4431
+ this.recordPartialMixedFeedbackAdmission(record, 'continuation-not-admitted');
4432
+ }
4433
+ if (admittedContinuation)
4434
+ this.notifyWorkItemReplyPending();
4230
4435
  void this.drainWorkItemFeedback().catch(() => { });
4231
4436
  return { duplicate: accepted.duplicate, inboxId: record.id, status: 'processed' };
4232
4437
  }
4233
- if (feedback !== 'ordinary' && this.admitWorkItemReply(record, binding)) {
4438
+ // A pure recognized result never becomes execution when feedback permission
4439
+ // is absent; only an explicit continuation in the same reply may proceed.
4440
+ const continuationAllowed = feedback.kind === 'none'
4441
+ || (feedback.kind === 'ordinary' && splitContinuation);
4442
+ if (continuationAllowed && this.admitWorkItemReply(record, binding)) {
4234
4443
  const handedOff = this.deliveryStore.getInbox(record.id);
4235
4444
  this.notifyWorkItemReplyPending();
4445
+ if (feedback.kind === 'ordinary')
4446
+ this.recordPartialMixedFeedbackAdmission(record, 'feedback-not-admitted');
4236
4447
  return { duplicate: accepted.duplicate, inboxId: handedOff.id, status: handedOff.status };
4237
4448
  }
4449
+ if (feedback.kind === 'ordinary' && splitContinuation)
4450
+ this.recordPartialMixedFeedbackAdmission(record, 'neither-lane-admitted');
4238
4451
  record = this.deliveryStore.queueInbox(record.id, binding.id);
4239
4452
  return { duplicate: accepted.duplicate, inboxId: record.id, status: record.status };
4240
4453
  });
4241
4454
  }
4455
+ /**
4456
+ * Deterministic backend classification of one authenticated owner reply to an
4457
+ * exact report. The legacy closed whole-sentence grammar stays in force unless
4458
+ * the widened coverage opt-in is on; no caller text is reinterpreted as an
4459
+ * instruction, and no model is called.
4460
+ */
4242
4461
  parseWorkItemFeedback(envelope) {
4243
4462
  if ((envelope.attachments?.length ?? 0) !== 0)
4244
4463
  return undefined;
4245
4464
  if (envelope.kind === 'text') {
4246
4465
  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 };
4466
+ if (!this.widenedNaturalFeedbackCoverage()) {
4467
+ if (!/^(?:问题解决了|已经解决了|现在解决了|还是不行|还没解决|问题还没解决|撤回刚才的任务反馈|撤回我刚才的任务反馈)[。.!!]?$/u.test(text))
4468
+ return undefined;
4469
+ const value = parseNaturalObjectiveFeedback(text);
4470
+ return value === undefined ? undefined : { selection: { kind: 'natural', value }, continuation: false };
4471
+ }
4472
+ const parsed = parseNaturalOwnerReply(text);
4473
+ return parsed === undefined ? undefined : {
4474
+ selection: { kind: 'natural', value: parsed.status },
4475
+ continuation: parsed.continuation && this.mixedNaturalFeedbackIntent(),
4476
+ };
4251
4477
  }
4252
4478
  const command = parseDeliveryCommand(envelope);
4253
4479
  if (command?.name !== 'feedback')
4254
4480
  return undefined;
4255
4481
  const parsed = parseFeedbackCommand(command.rawInput);
4256
4482
  return ['objective', 'objective-status', 'objective-revision'].includes(parsed.kind)
4257
- ? { kind: 'explicit', command: parsed } : undefined;
4483
+ ? { selection: { kind: 'explicit', command: parsed }, continuation: false }
4484
+ : undefined;
4258
4485
  }
4259
4486
  workItemFeedbackPermission(binding, registration) {
4260
4487
  return this.policy.evaluate({ subject: { kind: 'external', id: externalPrincipalId(binding.principal), workspace: binding.workspace, principal: externalPrincipalId(binding.principal) },
4261
4488
  action: 'feedback-work-items', resource: { kind: 'automation', id: registration.continuationAutomationId }, context: { initiator: 'external' } }).effect === 'allow';
4262
4489
  }
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';
4490
+ /**
4491
+ * Inspect one Inbox as an authenticated owner result reply to an exact report.
4492
+ * `ready` carries the frozen source plus its live admission fence; `ordinary`
4493
+ * keeps the historical "recognized result but separate feedback permission
4494
+ * absent" outcome and never admits the result lane by itself. The
4495
+ * `continuation` flag is a deterministic classification only.
4496
+ */
4497
+ inspectOwnerWorkItemFeedback(inbox, binding) {
4498
+ const registration = this.currentWorkItemReplyConsumer();
4499
+ const parsed = this.parseWorkItemFeedback(inbox.envelope);
4500
+ if (!registration || registration.feedbackEnabled !== true || parsed === undefined || !inbox.envelope.metadata?.replyToProviderMessageId)
4501
+ return { kind: 'none' };
4267
4502
  const report = this.deliveryStore.workItemReplies.replyReport(inbox.channel, inbox.account, inbox.envelope.metadata.replyToProviderMessageId);
4268
4503
  if (!report || !['accepted', 'delivered', 'read'].includes(report.outbox.status) || !report.outbox.providerMessageId
4269
4504
  || workItemReplyCanonical(report.outbox.intent.target.conversation) !== workItemReplyCanonical(inbox.envelope.conversation)
4270
4505
  || workItemReplyCanonical(report.outbox.intent.target.principal) !== workItemReplyCanonical(binding.principal))
4271
- return 'none';
4506
+ return { kind: 'none' };
4272
4507
  let target;
4273
4508
  try {
4274
4509
  target = this.inspectWorkItemReport({ sourceId: registration.sourceId, itemId: report.target.itemId, reportRevision: report.target.reportRevision,
@@ -4276,25 +4511,30 @@ export class AssistantDeliveryService extends Service {
4276
4511
  format: report.outbox.intent.format === 'markdown' ? 'markdown' : 'plain' }, true);
4277
4512
  }
4278
4513
  catch {
4279
- return 'none';
4514
+ return { kind: 'none' };
4280
4515
  }
4281
4516
  if (workItemReplyCanonical(target) !== workItemReplyCanonical(report.target))
4282
- return 'none';
4517
+ return { kind: 'none' };
4518
+ const continuation = parsed.continuation;
4283
4519
  // A recognized result must not be converted into execution when its separate feedback permission is absent.
4284
4520
  if (!this.workItemFeedbackPermission(binding, registration))
4285
- return 'ordinary';
4521
+ return { kind: 'ordinary', continuation };
4286
4522
  const source = { protocol: 'assistant-delivery/work-item-feedback/v1', inboxId: inbox.id, envelopeHash: inbox.envelopeHash,
4287
4523
  inputText: inbox.envelope.text, inputSha256: createHash('sha256').update(inbox.envelope.text).digest('hex'), admissionCursor: inbox.admissionCursor, occurredAt: inbox.receivedAt,
4288
4524
  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,
4525
+ 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
4526
  operationId: `owner-work-item-feedback:${createHash('sha256').update(JSON.stringify([inbox.id, inbox.envelopeHash])).digest('hex')}` };
4291
- this.deliveryStore.workItemFeedback.admit(source, () => {
4527
+ const fence = () => {
4292
4528
  const current = this.currentWorkItemReplyConsumer(), owner = this.ownerLineageForBinding(binding), active = this.deliveryStore.getBinding(binding.id);
4293
4529
  if (current !== registration || current?.feedbackEnabled !== true || workItemReplyCanonical(owner) !== workItemReplyCanonical(source.owner)
4294
4530
  || workItemReplyCanonical(active) !== workItemReplyCanonical(binding) || !this.workItemFeedbackPermission(binding, registration))
4295
4531
  throw new Error('feedback admission authority changed');
4296
- });
4297
- return 'handled';
4532
+ };
4533
+ return { kind: 'ready', source, fence, continuation };
4534
+ }
4535
+ /** One deterministic, non-durable record when a mixed reply could only admit one lane. */
4536
+ recordPartialMixedFeedbackAdmission(inbox, reason) {
4537
+ this.context.logger.warn(`assistant-delivery: mixed owner feedback ${inbox.id} admitted partially (${reason})`);
4298
4538
  }
4299
4539
  /** Passing an explicit registration makes this the callback-free final local authority check. */
4300
4540
  workItemFeedbackFence(intent, recorded = false, expectedRegistration) {
@@ -4596,7 +4836,11 @@ export class AssistantDeliveryService extends Service {
4596
4836
  || !/^[a-z][a-z0-9-]{0,54}$/u.test(principal.channel))
4597
4837
  return undefined;
4598
4838
  const key = { channel: principal.channel, account: principal.account, tenant: principal.tenant, user: principal.user };
4599
- const senderAllowlisted = this.deliveryStore.isAuthorizedPrincipal(key);
4839
+ // Only the owner role is a trusted intake sender. A linked peer bot is
4840
+ // authorized to converse, but its messages must stay `inferred`/filtered
4841
+ // rather than entering the owner work-item allowlist.
4842
+ const principalRecord = this.deliveryStore.getPrincipal(key);
4843
+ const senderAllowlisted = principalRecord?.status === 'active' && principalRecord.role === 'owner';
4600
4844
  // A follow-up counts only from an allowlisted sender replying, in the same
4601
4845
  // conversation, to a delivered WorkItems report.
4602
4846
  let threadFollowUp = false;
@@ -4879,32 +5123,43 @@ export class AssistantDeliveryService extends Service {
4879
5123
  action: 'continue-work-items-from-reply', resource: { kind: 'automation', id: registration.continuationAutomationId },
4880
5124
  context: { initiator: 'external' } }).effect === 'allow';
4881
5125
  }
4882
- admitWorkItemReply(inbox, binding) {
5126
+ /**
5127
+ * Validate the continuation lane of one reply to an exact report without
5128
+ * writing it. Mixed-intent admission reuses this report inside its combined
5129
+ * transaction; the ordinary path admits it through `admitWorkItemReply`.
5130
+ */
5131
+ prepareWorkItemReply(inbox, binding) {
4883
5132
  const registration = this.currentWorkItemReplyConsumer();
4884
5133
  const envelope = inbox.envelope;
4885
5134
  if (!registration || registration.continuationEnabled === false || envelope.kind !== 'text' || envelope.text.trim().startsWith('/') || !envelope.text
4886
5135
  || (envelope.attachments?.length ?? 0) > 0 || !envelope.metadata?.replyToProviderMessageId)
4887
- return false;
5136
+ return undefined;
4888
5137
  const report = this.deliveryStore.workItemReplies.replyReport(envelope.channel, envelope.account, envelope.metadata.replyToProviderMessageId);
4889
5138
  const outbox = report?.outbox;
4890
5139
  if (!report || !outbox || !['accepted', 'delivered', 'read'].includes(outbox.status) || workItemReplyCanonical(outbox.intent.target.principal) !== workItemReplyCanonical(binding.principal)
4891
5140
  || workItemReplyCanonical(outbox.intent.target.conversation) !== workItemReplyCanonical(envelope.conversation))
4892
- return false;
5141
+ return undefined;
4893
5142
  try {
4894
5143
  const target = this.inspectWorkItemReport({ sourceId: registration.sourceId, itemId: report.target.itemId,
4895
5144
  reportRevision: report.target.reportRevision, bindingId: outbox.intent.bindingId, idempotencyKey: outbox.intent.idempotencyKey,
4896
5145
  text: outbox.intent.text, format: outbox.intent.format === 'markdown' ? 'markdown' : 'plain' }, true);
4897
5146
  if (workItemReplyCanonical(target) !== workItemReplyCanonical(report.target))
4898
- return false;
5147
+ return undefined;
4899
5148
  if (registration.canContinueReport?.({ itemId: target.itemId, reportRevision: target.reportRevision, bindingId: outbox.intent.bindingId,
4900
5149
  idempotencyKey: outbox.intent.idempotencyKey, text: outbox.intent.text, format: outbox.intent.format === 'markdown' ? 'markdown' : 'plain' }) === false)
4901
- return false;
5150
+ return undefined;
4902
5151
  if (this.currentWorkItemReplyConsumer() !== registration || !this.workItemReplyPermission(binding, registration))
4903
- return false;
5152
+ return undefined;
4904
5153
  }
4905
5154
  catch {
4906
- return false;
5155
+ return undefined;
4907
5156
  }
5157
+ return report;
5158
+ }
5159
+ admitWorkItemReply(inbox, binding) {
5160
+ const report = this.prepareWorkItemReply(inbox, binding);
5161
+ if (report === undefined)
5162
+ return false;
4908
5163
  // From here a failed durable handoff is an error, never ordinary fallback.
4909
5164
  this.deliveryStore.workItemReplies.admit(inbox.id, binding, report);
4910
5165
  return true;
@@ -5115,7 +5370,7 @@ export class AssistantDeliveryService extends Service {
5115
5370
  throw new AssistantDeliveryError('runtime-conflict', 'invalid notification outcome lookup');
5116
5371
  }
5117
5372
  return Object.freeze({ protocol: NOTIFICATION_OUTCOME_FACTS_PROTOCOL,
5118
- facts: this.deliveryStore.attentionAudit.outcomeFactsFor([...new Set(input.outboxIds)]), hasMore: false, nextAfterSequence: null });
5373
+ facts: this.notificationOutcomeFacts([...new Set(input.outboxIds)]), hasMore: false, nextAfterSequence: null });
5119
5374
  }
5120
5375
  const afterSequence = input.afterSequence ?? 0;
5121
5376
  const limit = input.limit ?? 100;
@@ -5123,10 +5378,24 @@ export class AssistantDeliveryService extends Service {
5123
5378
  throw new AssistantDeliveryError('runtime-conflict', 'invalid notification outcome scan');
5124
5379
  }
5125
5380
  const page = this.deliveryStore.attentionAudit.list(afterSequence, limit);
5126
- const facts = this.deliveryStore.attentionAudit.outcomeFactsFor(page.entries.map(entry => entry.outboxId));
5381
+ const facts = this.notificationOutcomeFacts(page.entries.map(entry => entry.outboxId));
5127
5382
  return Object.freeze({ protocol: NOTIFICATION_OUTCOME_FACTS_PROTOCOL, facts, hasMore: page.hasMore,
5128
5383
  nextAfterSequence: page.entries.length === 0 ? null : page.entries.at(-1).sequence });
5129
5384
  }
5385
+ /**
5386
+ * Attach the registered adapter's read-receipt completeness to each fact.
5387
+ * The Delivery store owns transport facts but not adapter capabilities; only
5388
+ * this read boundary can combine them. An undeclared capability stays absent
5389
+ * so older Evaluation classifiers keep their pinned v1 digests.
5390
+ */
5391
+ notificationOutcomeFacts(outboxIds) {
5392
+ return this.deliveryStore.attentionAudit.outcomeFactsFor(outboxIds).map(fact => {
5393
+ const binding = this.deliveryStore.getBinding(fact.bindingId);
5394
+ const readReceipts = binding === undefined ? undefined
5395
+ : this.registry.get(binding.conversation.channel, binding.conversation.account)?.capabilities.readReceipts;
5396
+ return readReceipts === undefined ? fact : Object.freeze({ ...fact, readReceipts });
5397
+ });
5398
+ }
5130
5399
  /**
5131
5400
  * Read-only WP19 stage-2c progress audit page in append order; never exposes
5132
5401
  * progress text. Entries are process-local (`durable: false`), bounded, and
@@ -5496,6 +5765,148 @@ export class AssistantDeliveryService extends Service {
5496
5765
  return undefined;
5497
5766
  return Object.freeze({ outboxId: record.id, status: record.status });
5498
5767
  }
5768
+ /**
5769
+ * Register the slice-5 reply consumer for one owner-notice source (see
5770
+ * `OwnerNotificationReply`). The service in the `authority` slot must vouch
5771
+ * for the exact object; one live consumer per (authority, source). A
5772
+ * registration its authority no longer vouches for is superseded by the
5773
+ * next vouched one, so a restarted owner regains its replies without a new
5774
+ * notice. Registering never sends anything and grants no send authority.
5775
+ */
5776
+ registerOwnerNotificationReply(registration) {
5777
+ this.assertActive();
5778
+ if (!registration || registration.protocol !== ownerNotificationReplyProtocol
5779
+ || typeof registration.authority !== 'string' || !ownerNotificationAuthorityPattern.test(registration.authority)
5780
+ || typeof registration.sourceId !== 'string' || typeof registration.idempotencyKeyPrefix !== 'string'
5781
+ || registration.idempotencyKeyPrefix.length < 1 || registration.idempotencyKeyPrefix.length > 128
5782
+ || typeof registration.consume !== 'function') {
5783
+ throw new AssistantDeliveryError('runtime-conflict', 'invalid owner notification reply consumer');
5784
+ }
5785
+ const authority = registration.authority;
5786
+ const sourceId = canonicalBackgroundSourceId(registration.sourceId);
5787
+ if (!this.replyVouched(authority, registration))
5788
+ throw new AssistantDeliveryError('runtime-conflict', 'owner notification reply is not owned by its authority');
5789
+ const key = ownerNotificationWithdrawalKey(authority, sourceId);
5790
+ const existing = this.ownerNotificationReplies.get(key);
5791
+ if (existing !== undefined) {
5792
+ if (this.replyVouched(existing.authority, existing.registration))
5793
+ throw new AssistantDeliveryError('runtime-conflict', 'owner notification reply already registered');
5794
+ this.ownerNotificationReplies.delete(key);
5795
+ }
5796
+ const entry = Object.freeze({ authority, sourceId, registration });
5797
+ this.ownerNotificationReplies.set(key, entry);
5798
+ let live = true;
5799
+ return Object.freeze({ unregister: () => {
5800
+ if (!live)
5801
+ return;
5802
+ live = false;
5803
+ if (this.ownerNotificationReplies.get(key) === entry)
5804
+ this.ownerNotificationReplies.delete(key);
5805
+ } });
5806
+ }
5807
+ /** The service now in `authority`'s slot vouches for this exact reply registration. */
5808
+ replyVouched(authority, registration) {
5809
+ try {
5810
+ const owner = this.context.get(authority, false);
5811
+ return typeof owner?.ownsOwnerNotificationReply === 'function' && owner.ownsOwnerNotificationReply(registration) === true;
5812
+ }
5813
+ catch {
5814
+ return false;
5815
+ }
5816
+ }
5817
+ /**
5818
+ * Slice 5 matcher: bind one authenticated inbound text reply to exactly one
5819
+ * accepted/delivered/read owner notice through `replyToProviderMessageId`,
5820
+ * require the live registered consumer's source and idempotency-key prefix,
5821
+ * an active owner binding/principal with the notice's frozen owner lineage,
5822
+ * and an `allow` external `answer-attention-question` Policy decision. On
5823
+ * `applied` the Inbox is marked processed; `transient` leaves it pending for
5824
+ * a retry. Everything else falls through to ordinary inbound handling.
5825
+ */
5826
+ inspectOwnerQuestionReply(inbox, binding) {
5827
+ this.assertActive();
5828
+ const envelope = inbox.envelope;
5829
+ const replyTo = envelope.metadata?.replyToProviderMessageId;
5830
+ if (typeof replyTo !== 'string' || replyTo.length === 0 || envelope.kind !== 'text' || (envelope.attachments?.length ?? 0) > 0)
5831
+ return 'none';
5832
+ if (typeof envelope.text !== 'string' || envelope.text.length === 0 || Buffer.byteLength(envelope.text, 'utf8') > 256)
5833
+ return 'none';
5834
+ // The reply must come from the active owner binding itself, not another
5835
+ // conversation that happens to quote the provider message id.
5836
+ if (binding.status !== 'active' || externalPrincipalId(binding.principal) !== externalPrincipalId(envelope.principal)
5837
+ || workItemReplyCanonical(envelope.conversation) !== workItemReplyCanonical(binding.conversation))
5838
+ return 'none';
5839
+ const active = this.deliveryStore.getActiveBinding(binding.conversation);
5840
+ if (active?.id !== binding.id)
5841
+ return 'none';
5842
+ const owner = this.deliveryStore.getPrincipal(binding.principal);
5843
+ if (owner?.status !== 'active' || owner.role !== 'owner')
5844
+ return 'none';
5845
+ let outbox;
5846
+ try {
5847
+ outbox = this.deliveryStore.getUniqueOutboxByProviderMessage(inbox.channel, inbox.account, replyTo);
5848
+ }
5849
+ catch {
5850
+ return 'none';
5851
+ }
5852
+ if (outbox === undefined || !['accepted', 'delivered', 'read'].includes(outbox.status))
5853
+ return 'none';
5854
+ const metadata = outbox.intent.metadata;
5855
+ if (metadata?.['dsh.native-notice'] !== 'v1' || typeof metadata['dsh.native-notice.sourceId'] !== 'string'
5856
+ || typeof metadata['dsh.native-notice.withdrawalAuthority'] !== 'string')
5857
+ return 'none';
5858
+ let sourceId;
5859
+ try {
5860
+ sourceId = canonicalBackgroundSourceId(metadata['dsh.native-notice.sourceId']);
5861
+ }
5862
+ catch {
5863
+ return 'none';
5864
+ }
5865
+ const entry = this.ownerNotificationReplies.get(ownerNotificationWithdrawalKey(metadata['dsh.native-notice.withdrawalAuthority'], sourceId));
5866
+ if (entry === undefined || !this.replyVouched(entry.authority, entry.registration))
5867
+ return 'none';
5868
+ const registration = entry.registration;
5869
+ if (!outbox.intent.idempotencyKey.startsWith(registration.idempotencyKeyPrefix))
5870
+ return 'none';
5871
+ if (workItemReplyCanonical(outbox.intent.target.conversation) !== workItemReplyCanonical(envelope.conversation)
5872
+ || workItemReplyCanonical(outbox.intent.target.principal) !== workItemReplyCanonical(envelope.principal))
5873
+ return 'none';
5874
+ // The notice froze an exact owner row; a re-paired/replaced owner does not
5875
+ // inherit permission to answer the old owner's question.
5876
+ if (metadata['dsh.native-notice.ownerRecordId'] !== owner.id
5877
+ || metadata['dsh.native-notice.ownerVersion'] !== String(owner.version))
5878
+ return 'none';
5879
+ const decision = this.policy.evaluate({
5880
+ subject: { kind: 'external', id: externalPrincipalId(binding.principal), workspace: binding.workspace, principal: externalPrincipalId(binding.principal) },
5881
+ action: 'answer-attention-question', resource: { kind: 'message', id: binding.id }, context: { initiator: 'external' },
5882
+ });
5883
+ if (decision.effect !== 'allow')
5884
+ return 'none';
5885
+ let outcome;
5886
+ try {
5887
+ outcome = registration.consume(Object.freeze({
5888
+ inboxId: inbox.id, envelopeHash: inbox.envelopeHash, outboxId: outbox.id, providerMessageId: replyTo,
5889
+ idempotencyKey: outbox.intent.idempotencyKey, sourceId, principalId: externalPrincipalId(binding.principal),
5890
+ text: envelope.text, receivedAt: inbox.receivedAt,
5891
+ }));
5892
+ }
5893
+ catch {
5894
+ return 'transient';
5895
+ }
5896
+ if (outcome === 'ignored')
5897
+ return 'none';
5898
+ if (outcome !== 'applied')
5899
+ return 'transient';
5900
+ try {
5901
+ this.deliveryStore.markInboxProcessed(inbox.id, binding.id);
5902
+ }
5903
+ catch {
5904
+ // The consumer's own durable effect already committed; if recording that
5905
+ // here fails, never fall through and send the authorization to the Agent.
5906
+ return this.deliveryStore.getInbox(inbox.id)?.status === 'processed' ? 'handled' : 'transient';
5907
+ }
5908
+ return 'handled';
5909
+ }
5499
5910
  /**
5500
5911
  * Dedicated Automation result lane. Every learning field is derived from an
5501
5912
  * authoritative Automations receipt; callers cannot supply metadata,
@@ -7181,6 +7592,13 @@ export class AssistantDeliveryService extends Service {
7181
7592
  if (binding === undefined)
7182
7593
  continue;
7183
7594
  if (!reset && !stop) {
7595
+ // A pending owner-question reply is finished (or held for a retry)
7596
+ // before it can be queued to the Agent.
7597
+ const attentionReply = this.inspectOwnerQuestionReply(pending, binding);
7598
+ if (attentionReply === 'handled')
7599
+ continue;
7600
+ if (attentionReply === 'transient')
7601
+ return;
7184
7602
  this.deliveryStore.queueInbox(pending.id, binding.id);
7185
7603
  continue;
7186
7604
  }