@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/README.md +16 -3
- package/lib/agent-runtime.d.ts.map +1 -1
- package/lib/agent-runtime.js +16 -9
- package/lib/agent-runtime.js.map +1 -1
- package/lib/feedback-command.d.ts +23 -0
- package/lib/feedback-command.d.ts.map +1 -1
- package/lib/feedback-command.js +176 -0
- package/lib/feedback-command.js.map +1 -1
- package/lib/index.d.ts +3 -3
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -2
- package/lib/index.js.map +1 -1
- package/lib/service.d.ts +137 -1
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +335 -33
- package/lib/service.js.map +1 -1
- package/lib/store.d.ts +43 -0
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +142 -4
- package/lib/store.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/lib/work-item-feedback-store.d.ts +7 -0
- package/lib/work-item-feedback-store.d.ts.map +1 -1
- package/lib/work-item-feedback-store.js +19 -12
- package/lib/work-item-feedback-store.js.map +1 -1
- package/lib/work-item-replies-store.d.ts +8 -0
- package/lib/work-item-replies-store.d.ts.map +1 -1
- package/lib/work-item-replies-store.js +43 -33
- package/lib/work-item-replies-store.js.map +1 -1
- package/package.json +10 -10
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
|
-
?
|
|
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
|
-
&&
|
|
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
|
-
|
|
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
|
-
|
|
4229
|
-
|
|
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
|
-
|
|
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 (
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
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 } :
|
|
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
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
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
|
-
|
|
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 '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
5048
|
+
return undefined;
|
|
4902
5049
|
if (this.currentWorkItemReplyConsumer() !== registration || !this.workItemReplyPermission(binding, registration))
|
|
4903
|
-
return
|
|
5050
|
+
return undefined;
|
|
4904
5051
|
}
|
|
4905
5052
|
catch {
|
|
4906
|
-
return
|
|
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
|
}
|