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