@dsh-enhanced/assistant-delivery 0.1.6 → 0.1.12
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 +106 -7
- package/cordis.patch.yml +1 -0
- package/lib/agent-runtime.d.ts +23 -2
- package/lib/agent-runtime.d.ts.map +1 -1
- package/lib/agent-runtime.js +317 -2
- package/lib/agent-runtime.js.map +1 -1
- package/lib/canonical.d.ts +11 -1
- package/lib/canonical.d.ts.map +1 -1
- package/lib/canonical.js +107 -0
- package/lib/canonical.js.map +1 -1
- package/lib/coordinator.d.ts +3 -1
- package/lib/coordinator.d.ts.map +1 -1
- package/lib/coordinator.js +49 -21
- package/lib/coordinator.js.map +1 -1
- package/lib/feedback-command.d.ts +60 -0
- package/lib/feedback-command.d.ts.map +1 -0
- package/lib/feedback-command.js +338 -0
- package/lib/feedback-command.js.map +1 -0
- package/lib/index.d.ts +4 -3
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +4 -3
- package/lib/index.js.map +1 -1
- package/lib/learning-command.d.ts +40 -0
- package/lib/learning-command.d.ts.map +1 -0
- package/lib/learning-command.js +57 -0
- package/lib/learning-command.js.map +1 -0
- package/lib/service.d.ts +149 -6
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +1945 -34
- package/lib/service.js.map +1 -1
- package/lib/session-commands.d.ts +14 -0
- package/lib/session-commands.d.ts.map +1 -1
- package/lib/session-commands.js +30 -0
- package/lib/session-commands.js.map +1 -1
- package/lib/sqlite.d.ts +1 -1
- package/lib/sqlite.d.ts.map +1 -1
- package/lib/sqlite.js +648 -20
- package/lib/sqlite.js.map +1 -1
- package/lib/store.d.ts +375 -3
- package/lib/store.d.ts.map +1 -1
- package/lib/store.js +2956 -74
- package/lib/store.js.map +1 -1
- package/lib/types.d.ts +407 -0
- package/lib/types.d.ts.map +1 -1
- package/lib/types.js +9 -1
- package/lib/types.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.d.ts.map +1 -1
- package/lib/version.js +1 -1
- package/lib/version.js.map +1 -1
- package/lib/workflow-auto-producer.d.ts +37 -0
- package/lib/workflow-auto-producer.d.ts.map +1 -0
- package/lib/workflow-auto-producer.js +58 -0
- package/lib/workflow-auto-producer.js.map +1 -0
- package/lib/workflow-command.d.ts +16 -0
- package/lib/workflow-command.d.ts.map +1 -0
- package/lib/workflow-command.js +52 -0
- package/lib/workflow-command.js.map +1 -0
- package/package.json +23 -3
package/lib/service.js
CHANGED
|
@@ -4,13 +4,220 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
4
4
|
import { Context, Service } from '@deepseek-ai/cordis';
|
|
5
5
|
import Schema from '@deepseek-ai/schemastery';
|
|
6
6
|
import { approvalReviewerOf, isAutoReviewEscalation, } from '@dsh-enhanced/assistant-policy';
|
|
7
|
+
import { TRUSTED_EVALUATION_PRODUCER_PROTOCOL, } from '@dsh-enhanced/assistant-evaluation';
|
|
8
|
+
import { ASSISTANT_GROWTH_CONTRACT_VERSION, growthObjectDigest, validateResolvedWorkflowAutomationTemplate, validateWorkflowAutomationTemplate, validateWorkflowScope, validateWorkflowTraceProjectionReceipt, workflowArgumentShapeDigest, } from '@dsh-enhanced/assistant-growth-contract';
|
|
7
9
|
import { DeliveryAdapterRegistry, DeliveryCoordinator, InboundCoordinator, } from './coordinator.js';
|
|
8
10
|
import { DeliveryStore, DeliveryStoreError } from './store.js';
|
|
9
11
|
import { DshDeliveryRuntime } from './agent-runtime.js';
|
|
10
12
|
import { InboundImageMaterializer } from './inbound-images.js';
|
|
11
13
|
import { registerDeliveryTools } from './tools.js';
|
|
12
|
-
import { canonicalConversation, externalPrincipalId } from './canonical.js';
|
|
14
|
+
import { bindingMatchesOwnerRoute, canonicalBackgroundSourceId as canonicalHostBackgroundSourceId, canonicalConversation, canonicalLocalOperatorId, canonicalOwnerRouteAuthority, externalPrincipalId, ownerRouteAuthorityHash, ownerRouteBindingSnapshot, } from './canonical.js';
|
|
13
15
|
import { isExactDeliveryCommand, parseDeliveryCommand } from './session-commands.js';
|
|
16
|
+
import { deliveryT1PreferenceKeys, deliveryT1PreferenceValues } from './learning-command.js';
|
|
17
|
+
import { feedbackSignalInput, classifyNaturalPreferenceDirective, observedResponseLanguage, } from './feedback-command.js';
|
|
18
|
+
import { DELIVERY_PREFERENCE_PROJECTION_PROTOCOL, TRUSTED_DELIVERY_PRESENTATION_PRODUCER_PROTOCOL, } from './types.js';
|
|
19
|
+
const trustedDeliveryPreferenceProducers = new WeakSet();
|
|
20
|
+
const deliveryPreferenceProducerProbe = Symbol('assistant-delivery.preference-producer-probe');
|
|
21
|
+
/**
|
|
22
|
+
* Process-local authenticity against accidental service-slot collisions. The
|
|
23
|
+
* private symbol is never exported, so copying the public method shape is not
|
|
24
|
+
* enough to solicit a genuine registration. This is not an OS/process sandbox
|
|
25
|
+
* boundary against code already executing inside the trusted Host process.
|
|
26
|
+
*/
|
|
27
|
+
export function isTrustedDeliveryPreferenceProducer(value) {
|
|
28
|
+
if (typeof value !== 'object' || value === null)
|
|
29
|
+
return false;
|
|
30
|
+
try {
|
|
31
|
+
const probe = value[deliveryPreferenceProducerProbe];
|
|
32
|
+
if (typeof probe !== 'function')
|
|
33
|
+
return false;
|
|
34
|
+
const exact = probe();
|
|
35
|
+
return typeof exact === 'object' && exact !== null
|
|
36
|
+
&& trustedDeliveryPreferenceProducers.has(exact);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const learningMetadataPrefix = 'dsh.learning.';
|
|
43
|
+
const automationObjectiveFeedbackFooter = [
|
|
44
|
+
'',
|
|
45
|
+
'---',
|
|
46
|
+
'任务结果反馈:直接回复本消息并发送 `/feedback achieved`、`/feedback partial` 或 `/feedback not-achieved`。',
|
|
47
|
+
'`helpful` 等只记录偏好,不会被当成任务成败。',
|
|
48
|
+
].join('\n');
|
|
49
|
+
function isAutomationDeliveryEvidenceResolver(value) {
|
|
50
|
+
return typeof value === 'object' && value !== null
|
|
51
|
+
&& typeof value.resolveDeliveryEvidence === 'function';
|
|
52
|
+
}
|
|
53
|
+
function registrationOwnedByEvaluation(registration) {
|
|
54
|
+
const candidate = registration;
|
|
55
|
+
try {
|
|
56
|
+
return typeof candidate.owner === 'object' && candidate.owner !== null
|
|
57
|
+
&& typeof candidate.owner.ownsTrustedDeliveryEvaluationRegistration === 'function'
|
|
58
|
+
&& candidate.owner.ownsTrustedDeliveryEvaluationRegistration(registration);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function registrationOwnedByPreference(registration) {
|
|
65
|
+
try {
|
|
66
|
+
return typeof registration.owner === 'object' && registration.owner !== null
|
|
67
|
+
&& typeof registration.owner.ownsDeliveryPreferenceRegistration === 'function'
|
|
68
|
+
&& registration.owner.ownsDeliveryPreferenceRegistration(registration);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function isTrustedDeliveryPresentationProducer(value) {
|
|
75
|
+
return typeof value === 'object' && value !== null
|
|
76
|
+
&& typeof value
|
|
77
|
+
.trustedDeliveryPresentationProducerGeneration === 'function'
|
|
78
|
+
&& typeof value
|
|
79
|
+
.registerTrustedDeliveryPresentationSink === 'function';
|
|
80
|
+
}
|
|
81
|
+
function validatePreferenceProjectionReceipts(receipts, events) {
|
|
82
|
+
if (!Array.isArray(receipts) || receipts.length !== events.length) {
|
|
83
|
+
throw Object.assign(new Error('preference projection receipt batch is invalid'), {
|
|
84
|
+
code: 'invalid-receipt',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const expected = new Set(events.map(event => event.idempotencyKey));
|
|
88
|
+
const received = new Set();
|
|
89
|
+
for (const receipt of receipts) {
|
|
90
|
+
if (receipt?.status !== 'recorded' || typeof receipt.idempotencyKey !== 'string'
|
|
91
|
+
|| !expected.has(receipt.idempotencyKey) || received.has(receipt.idempotencyKey)) {
|
|
92
|
+
throw Object.assign(new Error('preference projection receipt is invalid'), {
|
|
93
|
+
code: 'invalid-receipt',
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
received.add(receipt.idempotencyKey);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function validateLearningControlReceipt(value, request) {
|
|
100
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)
|
|
101
|
+
|| Object.getPrototypeOf(value) !== Object.prototype) {
|
|
102
|
+
throw new Error('learning control receipt is invalid');
|
|
103
|
+
}
|
|
104
|
+
const receipt = value;
|
|
105
|
+
if (receipt.action !== request.action || receipt.idempotencyKey !== request.idempotencyKey) {
|
|
106
|
+
throw new Error('learning control receipt identity is invalid');
|
|
107
|
+
}
|
|
108
|
+
if (receipt.outcome === 'stale') {
|
|
109
|
+
if (Object.keys(receipt).sort().join(',') !== 'action,idempotencyKey,outcome') {
|
|
110
|
+
throw new Error('learning stale receipt contains untrusted fields');
|
|
111
|
+
}
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (receipt.outcome !== 'applied' || typeof receipt.replayed !== 'boolean'
|
|
115
|
+
|| typeof receipt.state !== 'object' || receipt.state === null) {
|
|
116
|
+
throw new Error('learning control receipt outcome is invalid');
|
|
117
|
+
}
|
|
118
|
+
const expectedReceiptKeys = ['action', 'idempotencyKey', 'outcome', 'replayed', 'state'];
|
|
119
|
+
if (request.action === 'forget')
|
|
120
|
+
expectedReceiptKeys.push('deletedHypotheses', 'deletedSignals');
|
|
121
|
+
if (request.action === 'explain')
|
|
122
|
+
expectedReceiptKeys.push('explanation');
|
|
123
|
+
if (request.action === 'export')
|
|
124
|
+
expectedReceiptKeys.push('exportDocument');
|
|
125
|
+
if (request.action === 'rollback') {
|
|
126
|
+
expectedReceiptKeys.push('rolledBack');
|
|
127
|
+
if (receipt.rolledBack === true)
|
|
128
|
+
expectedReceiptKeys.push('rolledBackVersion');
|
|
129
|
+
}
|
|
130
|
+
if (Object.keys(receipt).sort().join(',') !== expectedReceiptKeys.sort().join(',')) {
|
|
131
|
+
throw new Error('learning control receipt contains untrusted fields');
|
|
132
|
+
}
|
|
133
|
+
const state = receipt.state;
|
|
134
|
+
if (Array.isArray(state) || Object.getPrototypeOf(state) !== Object.prototype
|
|
135
|
+
|| Object.keys(state).sort().join(',') !== [
|
|
136
|
+
'activeOverlays', 'administrativelyEnabled', 'collectionMode',
|
|
137
|
+
'effectiveActiveOverlays', 'hypotheses', 'mode', 'shadowHypotheses',
|
|
138
|
+
'signals', 'storedActiveOverlays',
|
|
139
|
+
].join(',')
|
|
140
|
+
|| !['active', 'disabled', 'paused'].includes(state.mode)
|
|
141
|
+
|| typeof state.administrativelyEnabled !== 'boolean'
|
|
142
|
+
|| !['active', 'paused'].includes(state.collectionMode)
|
|
143
|
+
|| [state.signals, state.hypotheses, state.storedActiveOverlays,
|
|
144
|
+
state.effectiveActiveOverlays, state.activeOverlays, state.shadowHypotheses]
|
|
145
|
+
.some(count => !Number.isSafeInteger(count) || count < 0)) {
|
|
146
|
+
throw new Error('learning control receipt state is invalid');
|
|
147
|
+
}
|
|
148
|
+
const expectedMode = state.administrativelyEnabled ? state.collectionMode : 'disabled';
|
|
149
|
+
const expectedEffective = state.administrativelyEnabled && state.collectionMode === 'active'
|
|
150
|
+
? state.storedActiveOverlays
|
|
151
|
+
: 0;
|
|
152
|
+
if (state.mode !== expectedMode || state.effectiveActiveOverlays !== expectedEffective
|
|
153
|
+
|| state.activeOverlays !== expectedEffective) {
|
|
154
|
+
throw new Error('learning control receipt state gates are inconsistent');
|
|
155
|
+
}
|
|
156
|
+
if (request.action === 'forget'
|
|
157
|
+
&& (!Number.isSafeInteger(receipt.deletedSignals) || receipt.deletedSignals < 0
|
|
158
|
+
|| !Number.isSafeInteger(receipt.deletedHypotheses) || receipt.deletedHypotheses < 0)) {
|
|
159
|
+
throw new Error('learning forget receipt is invalid');
|
|
160
|
+
}
|
|
161
|
+
if (request.action === 'explain') {
|
|
162
|
+
if (!Array.isArray(receipt.explanation))
|
|
163
|
+
throw new Error('learning explain receipt is invalid');
|
|
164
|
+
for (const item of receipt.explanation) {
|
|
165
|
+
if (typeof item !== 'object' || item === null
|
|
166
|
+
|| typeof item.key !== 'string' || typeof item.value !== 'string'
|
|
167
|
+
|| !['active', 'inactive', 'rolled-back', 'shadow', 'suppressed'].includes(item.state)
|
|
168
|
+
|| !Number.isSafeInteger(item.version) || item.version < 1
|
|
169
|
+
|| [item.supportingSignals, item.contradictingSignals, item.evidenceMass]
|
|
170
|
+
.some(count => !Number.isSafeInteger(count) || count < 0)) {
|
|
171
|
+
throw new Error('learning explain receipt is invalid');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (request.action === 'export') {
|
|
176
|
+
const document = receipt.exportDocument;
|
|
177
|
+
if (typeof document !== 'object' || document === null || Array.isArray(document)
|
|
178
|
+
|| Object.getPrototypeOf(document) !== Object.prototype
|
|
179
|
+
|| Object.keys(document).sort().join(',') !== 'format,records,version'
|
|
180
|
+
|| document.format !== 'dsh-preference-learning' || document.version !== 1
|
|
181
|
+
|| !Array.isArray(document.records)
|
|
182
|
+
|| document.records.length > Object.values(deliveryT1PreferenceValues)
|
|
183
|
+
.reduce((total, values) => total + values.length, 0)) {
|
|
184
|
+
throw new Error('learning export receipt is invalid');
|
|
185
|
+
}
|
|
186
|
+
let previousIdentity;
|
|
187
|
+
for (const item of document.records) {
|
|
188
|
+
if (typeof item !== 'object' || item === null || Array.isArray(item)
|
|
189
|
+
|| Object.getPrototypeOf(item) !== Object.prototype
|
|
190
|
+
|| Object.keys(item).sort().join(',') !== [
|
|
191
|
+
'contradictingSignals', 'evidenceMass', 'key', 'state',
|
|
192
|
+
'supportingSignals', 'value', 'version',
|
|
193
|
+
].join(',')
|
|
194
|
+
|| !deliveryT1PreferenceKeys.includes(item.key)
|
|
195
|
+
|| !deliveryT1PreferenceValues[item.key]
|
|
196
|
+
?.includes(item.value)
|
|
197
|
+
|| !['active', 'inactive', 'rolled-back', 'shadow', 'suppressed'].includes(item.state)
|
|
198
|
+
|| !Number.isSafeInteger(item.version) || item.version < 1
|
|
199
|
+
|| [item.supportingSignals, item.contradictingSignals, item.evidenceMass]
|
|
200
|
+
.some(count => !Number.isSafeInteger(count) || count < 0)) {
|
|
201
|
+
throw new Error('learning export receipt is invalid');
|
|
202
|
+
}
|
|
203
|
+
const identity = `${item.key}\0${item.value}`;
|
|
204
|
+
if (previousIdentity !== undefined && identity <= previousIdentity) {
|
|
205
|
+
throw new Error('learning export receipt is not canonically ordered');
|
|
206
|
+
}
|
|
207
|
+
previousIdentity = identity;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (request.action === 'rollback'
|
|
211
|
+
&& (typeof receipt.rolledBack !== 'boolean'
|
|
212
|
+
|| (receipt.rolledBack
|
|
213
|
+
? !Number.isSafeInteger(receipt.rolledBackVersion) || receipt.rolledBackVersion < 2
|
|
214
|
+
: receipt.rolledBackVersion !== undefined))) {
|
|
215
|
+
throw new Error('learning rollback receipt is invalid');
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function containsReservedLearningMetadata(metadata) {
|
|
219
|
+
return metadata !== undefined && Object.keys(metadata).some(key => key.startsWith(learningMetadataPrefix));
|
|
220
|
+
}
|
|
14
221
|
export class AssistantDeliveryError extends Error {
|
|
15
222
|
code;
|
|
16
223
|
constructor(code, message) {
|
|
@@ -41,10 +248,70 @@ const configSchema = Schema.object({
|
|
|
41
248
|
modelPickerTtlMs: Schema.number().step(1).min(60_000).max(86_400_000).default(900_000),
|
|
42
249
|
permissionPickerTtlMs: Schema.number().step(1).min(60_000).max(86_400_000).default(900_000),
|
|
43
250
|
toolApprovalTtlMs: Schema.number().step(1).min(1_000).max(300_000).default(300_000),
|
|
251
|
+
ownerRoutes: Schema.array(Schema.object({
|
|
252
|
+
id: Schema.string().min(1).required(),
|
|
253
|
+
conversation: Schema.object({
|
|
254
|
+
channel: Schema.string().min(1).required(),
|
|
255
|
+
account: Schema.string().min(1).required(),
|
|
256
|
+
tenant: Schema.string().min(1).required(),
|
|
257
|
+
kind: Schema.union(['dm', 'group']).required(),
|
|
258
|
+
chat: Schema.string().min(1).required(),
|
|
259
|
+
thread: Schema.string(),
|
|
260
|
+
}).required(),
|
|
261
|
+
principal: Schema.object({
|
|
262
|
+
channel: Schema.string().min(1).required(),
|
|
263
|
+
account: Schema.string().min(1).required(),
|
|
264
|
+
tenant: Schema.string().min(1).required(),
|
|
265
|
+
user: Schema.string().min(1).required(),
|
|
266
|
+
}).required(),
|
|
267
|
+
workspace: Schema.string().min(1).required(),
|
|
268
|
+
agentPreset: Schema.string().min(1).required(),
|
|
269
|
+
policyRef: Schema.string().min(1).required(),
|
|
270
|
+
minimumGeneration: Schema.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER).required(),
|
|
271
|
+
})).default([]),
|
|
44
272
|
});
|
|
45
273
|
function policyDenied(decision) {
|
|
46
274
|
return new AssistantDeliveryError('policy-denied', `assistant-delivery policy denied operation: ${decision.reasonCode}`);
|
|
47
275
|
}
|
|
276
|
+
function canonicalBackgroundSourceId(input) {
|
|
277
|
+
try {
|
|
278
|
+
return canonicalHostBackgroundSourceId(input);
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
throw new AssistantDeliveryError('runtime-conflict', 'background source identity is invalid');
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function canonicalOwnerRouteId(input) {
|
|
285
|
+
const id = canonicalBackgroundSourceId(input);
|
|
286
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$/u.test(id)) {
|
|
287
|
+
throw new AssistantDeliveryError('missing-binding', 'owner route authority id is invalid');
|
|
288
|
+
}
|
|
289
|
+
return id;
|
|
290
|
+
}
|
|
291
|
+
function canonicalRouteIdempotencyKey(input) {
|
|
292
|
+
if (typeof input !== 'string') {
|
|
293
|
+
throw new AssistantDeliveryError('runtime-conflict', 'owner route idempotency key is invalid');
|
|
294
|
+
}
|
|
295
|
+
const normalized = input.trim();
|
|
296
|
+
const hasControl = [...normalized].some(character => {
|
|
297
|
+
const code = character.codePointAt(0);
|
|
298
|
+
return code <= 0x1f || code === 0x7f;
|
|
299
|
+
});
|
|
300
|
+
if (normalized === '' || normalized.length > 512 || hasControl) {
|
|
301
|
+
throw new AssistantDeliveryError('runtime-conflict', 'owner route idempotency key is invalid');
|
|
302
|
+
}
|
|
303
|
+
return normalized;
|
|
304
|
+
}
|
|
305
|
+
/** One exact Policy identity shared by enqueue admission and dispatch rechecks. */
|
|
306
|
+
function authorizeOwnerRoute(policy, authority, sourceId, idempotencyKey) {
|
|
307
|
+
const operationDigest = createHash('sha256').update(JSON.stringify([
|
|
308
|
+
sourceId, authority.id, ownerRouteAuthorityHash(authority), idempotencyKey,
|
|
309
|
+
])).digest('hex');
|
|
310
|
+
return policy.authorize({ subject: { kind: 'background', id: sourceId,
|
|
311
|
+
workspace: authority.workspace, principal: externalPrincipalId(authority.principal) }, action: 'send',
|
|
312
|
+
resource: { kind: 'message', id: `route:${authority.id}` },
|
|
313
|
+
context: { initiator: 'background' } }, { idempotencyKey: `message-route-send:${operationDigest}` });
|
|
314
|
+
}
|
|
48
315
|
function pickerIncludesRoute(picker, route) {
|
|
49
316
|
const model = picker.models.find(candidate => candidate.provider === route.provider && candidate.id === route.model);
|
|
50
317
|
return model !== undefined && (route.reasoningEffort === undefined || model.effortIds.includes(route.reasoningEffort));
|
|
@@ -164,24 +431,49 @@ function exactToolApprovalCall(events, openTurn, callId, toolName) {
|
|
|
164
431
|
export class AssistantDeliveryService extends Service {
|
|
165
432
|
static Config = configSchema;
|
|
166
433
|
deliveryStore;
|
|
434
|
+
context;
|
|
167
435
|
policy;
|
|
168
436
|
registry;
|
|
169
437
|
outbound;
|
|
170
438
|
inbound;
|
|
171
439
|
config;
|
|
440
|
+
ownerRoutes;
|
|
441
|
+
ownerRouteGuard;
|
|
172
442
|
ownerId = `assistant-delivery-${randomUUID()}`;
|
|
173
443
|
bindingFlights = new Map();
|
|
174
444
|
conversationTransitions = new Map();
|
|
175
445
|
agentApprovalBindings = new WeakMap();
|
|
176
446
|
toolApprovalControllers = new Set();
|
|
177
447
|
modelSelectionWaiters = new Map();
|
|
448
|
+
preferenceFeedbackSink;
|
|
449
|
+
evaluationSink;
|
|
450
|
+
workflowTraceSink;
|
|
451
|
+
automationPresentationBinding;
|
|
452
|
+
evolutionPresentationBinding;
|
|
453
|
+
activePresentationRegistrations = new WeakSet();
|
|
454
|
+
evaluationProducerGeneration = `assistant-delivery:${randomUUID()}`;
|
|
455
|
+
preferenceProducerGeneration = `assistant-delivery-preference:${randomUUID()}`;
|
|
456
|
+
preferenceTurns = new WeakMap();
|
|
178
457
|
modelSelectionFlight;
|
|
458
|
+
presentationFlight;
|
|
459
|
+
workflowTraceFlight;
|
|
460
|
+
preferenceProjectionFlight;
|
|
461
|
+
workflowTraceRetryTimer;
|
|
462
|
+
preferenceProjectionRetryTimer;
|
|
179
463
|
modelSelectionRetryTimer;
|
|
180
464
|
runtime;
|
|
181
465
|
timer;
|
|
182
466
|
active = true;
|
|
183
467
|
constructor(ctx, input) {
|
|
184
468
|
super(ctx, 'assistantDelivery');
|
|
469
|
+
trustedDeliveryPreferenceProducers.add(this);
|
|
470
|
+
Object.defineProperty(this, deliveryPreferenceProducerProbe, {
|
|
471
|
+
value: () => this,
|
|
472
|
+
enumerable: false,
|
|
473
|
+
configurable: false,
|
|
474
|
+
writable: false,
|
|
475
|
+
});
|
|
476
|
+
this.context = ctx;
|
|
185
477
|
let config;
|
|
186
478
|
try {
|
|
187
479
|
config = AssistantDeliveryService.Config(input);
|
|
@@ -197,9 +489,40 @@ export class AssistantDeliveryService extends Service {
|
|
|
197
489
|
throw new Error('assistant-delivery: assistantPolicy service is required');
|
|
198
490
|
mkdirSync(config.spoolPath, { recursive: true, mode: 0o700 });
|
|
199
491
|
chmodSync(config.spoolPath, 0o700);
|
|
200
|
-
|
|
492
|
+
let ownerRoutes;
|
|
493
|
+
try {
|
|
494
|
+
ownerRoutes = config.ownerRoutes.map(route => canonicalOwnerRouteAuthority(route));
|
|
495
|
+
}
|
|
496
|
+
catch (error) {
|
|
497
|
+
throw new Error(`assistant-delivery: invalid owner route authority: ${String(error)}`, { cause: error });
|
|
498
|
+
}
|
|
499
|
+
if (new Set(ownerRoutes.map(route => route.id)).size !== ownerRoutes.length) {
|
|
500
|
+
throw new Error('assistant-delivery: invalid owner route authority: duplicate id');
|
|
501
|
+
}
|
|
502
|
+
const frozenOwnerRoutes = Object.freeze(ownerRoutes.map(route => Object.freeze({
|
|
503
|
+
...route,
|
|
504
|
+
conversation: Object.freeze({ ...route.conversation }),
|
|
505
|
+
principal: Object.freeze({ ...route.principal }),
|
|
506
|
+
})));
|
|
507
|
+
this.config = { ...config, ownerRoutes: frozenOwnerRoutes };
|
|
508
|
+
this.ownerRoutes = new Map(frozenOwnerRoutes.map(route => [route.id, route]));
|
|
201
509
|
this.policy = policy;
|
|
510
|
+
const ownerRouteGuard = {
|
|
511
|
+
ownerRoutes: [...this.ownerRoutes.values()],
|
|
512
|
+
authorize: ({ authority, sourceId, idempotencyKey }) => authorizeOwnerRoute(this.policy, authority, sourceId, idempotencyKey).effect === 'allow',
|
|
513
|
+
};
|
|
514
|
+
this.ownerRouteGuard = Object.freeze(ownerRouteGuard);
|
|
202
515
|
this.deliveryStore = new DeliveryStore({ path: config.databasePath, maxTextBytes: config.maxTextBytes });
|
|
516
|
+
ctx.on('agent/inbox/claimed', ({ agent, message, turn }) => {
|
|
517
|
+
this.capturePreferenceTurn(agent, message.source, turn);
|
|
518
|
+
});
|
|
519
|
+
ctx.on('agent/inbox/discarded', ({ agent }) => {
|
|
520
|
+
this.preferenceTurns.delete(agent);
|
|
521
|
+
});
|
|
522
|
+
ctx.on('agent/status', ({ agent, status }) => {
|
|
523
|
+
if (status === 'idle')
|
|
524
|
+
this.preferenceTurns.delete(agent);
|
|
525
|
+
});
|
|
203
526
|
this.registry = new DeliveryAdapterRegistry({
|
|
204
527
|
accept: async (envelope) => {
|
|
205
528
|
const result = await this.acceptInbound(envelope);
|
|
@@ -219,7 +542,9 @@ export class AssistantDeliveryService extends Service {
|
|
|
219
542
|
});
|
|
220
543
|
this.outbound = new DeliveryCoordinator({ store: this.deliveryStore, registry: this.registry,
|
|
221
544
|
ownerId: this.ownerId, leaseMs: config.leaseMs, maxAttempts: config.maxAttempts,
|
|
222
|
-
maxConcurrency: config.maxConcurrency, retryBaseMs: config.retryBaseMs, retryMaxMs: config.retryMaxMs
|
|
545
|
+
maxConcurrency: config.maxConcurrency, retryBaseMs: config.retryBaseMs, retryMaxMs: config.retryMaxMs,
|
|
546
|
+
ownerRouteGuard: this.ownerRouteGuard,
|
|
547
|
+
});
|
|
223
548
|
this.inbound = new InboundCoordinator({ store: this.deliveryStore, processor: () => this.runtime,
|
|
224
549
|
ownerId: this.ownerId, leaseMs: config.leaseMs, maxAttempts: config.maxAttempts,
|
|
225
550
|
maxConcurrency: config.maxConcurrency, retryBaseMs: config.retryBaseMs, retryMaxMs: config.retryMaxMs });
|
|
@@ -236,6 +561,13 @@ export class AssistantDeliveryService extends Service {
|
|
|
236
561
|
imageMaterializer,
|
|
237
562
|
isInboundAuthorized: (binding, envelope) => this.isInboundAuthorized(binding, envelope),
|
|
238
563
|
isPermissionController: (binding, envelope) => this.isPermissionController(binding, envelope),
|
|
564
|
+
isOwnerFeedbackController: (binding, envelope) => this.isOwnerFeedbackController(binding, envelope),
|
|
565
|
+
authorizeOwnerPreferenceFeedback: (binding, envelope, selections) => this.authorizeOwnerPreferenceFeedback(binding, envelope, selections),
|
|
566
|
+
dispatchPreferenceFeedback: events => this.dispatchPreferenceFeedback(events),
|
|
567
|
+
replyCompletedPreferenceTurn: (agent, binding, envelope, reply) => this.replyCompletedPreferenceTurn(agent, binding, envelope, reply),
|
|
568
|
+
dispatchObjectiveFeedback: (binding, envelope, objectiveStatus) => this.dispatchObjectiveFeedback(binding, envelope, objectiveStatus),
|
|
569
|
+
dispatchWorkflowCommand: (binding, envelope, command) => this.dispatchWorkflowCommand(binding, envelope, command),
|
|
570
|
+
dispatchLearningCommand: (binding, envelope, action, preferenceKey) => this.dispatchLearningCommand(binding, envelope, action, preferenceKey),
|
|
239
571
|
authorizePermissionReply: (binding, envelope) => this.authorizePermissionReply(binding, envelope),
|
|
240
572
|
beginModelCommand: conversation => this.deliveryStore.beginModelCommand(conversation),
|
|
241
573
|
commitModelCommand: input => this.deliveryStore.commitModelCommand(input),
|
|
@@ -258,26 +590,45 @@ export class AssistantDeliveryService extends Service {
|
|
|
258
590
|
...(reply.permissionPicker === undefined ? {} : { permissionPicker: reply.permissionPicker }),
|
|
259
591
|
});
|
|
260
592
|
},
|
|
261
|
-
reply: (agent, eventId, reply) => {
|
|
262
|
-
this.reply(agent, {
|
|
263
|
-
idempotencyKey: `inbound:${eventId}:reply`,
|
|
264
|
-
text: reply.text,
|
|
265
|
-
replyToEventId: eventId,
|
|
266
|
-
...(reply.format === undefined ? {} : { format: reply.format }),
|
|
267
|
-
...(reply.modelPicker === undefined ? {} : { modelPicker: reply.modelPicker }),
|
|
268
|
-
...(reply.permissionPicker === undefined ? {} : { permissionPicker: reply.permissionPicker }),
|
|
269
|
-
});
|
|
270
|
-
},
|
|
271
593
|
}));
|
|
272
594
|
void this.drainModelSelectionSettlements();
|
|
273
595
|
return unregister;
|
|
274
596
|
});
|
|
275
597
|
ctx.inject(['approval'], approvalCtx => approvalCtx.on('approval/request', (request, next) => this.requestToolApproval(approvalCtx, request, next)));
|
|
276
598
|
ctx.inject(['tools'], toolsCtx => registerDeliveryTools(toolsCtx, this));
|
|
599
|
+
// Presentation writes are intentionally reverse-bound. Delivery creates
|
|
600
|
+
// the publisher closure and hands it only to the exact live domain service;
|
|
601
|
+
// a public service-slot caller can no longer forge a terminal projection.
|
|
602
|
+
const currentAutomations = ctx.get('assistantAutomations');
|
|
603
|
+
if (isTrustedDeliveryPresentationProducer(currentAutomations)) {
|
|
604
|
+
this.bindPresentationProducer('assistant-automations', currentAutomations);
|
|
605
|
+
}
|
|
606
|
+
ctx.inject(['assistantAutomations'], automationsCtx => {
|
|
607
|
+
const producer = automationsCtx.get('assistantAutomations');
|
|
608
|
+
if (!isTrustedDeliveryPresentationProducer(producer))
|
|
609
|
+
return;
|
|
610
|
+
return this.bindPresentationProducer('assistant-automations', producer);
|
|
611
|
+
});
|
|
612
|
+
const currentEvolution = ctx.get('assistantEvolution');
|
|
613
|
+
if (isTrustedDeliveryPresentationProducer(currentEvolution)) {
|
|
614
|
+
this.bindPresentationProducer('assistant-evolution', currentEvolution);
|
|
615
|
+
}
|
|
616
|
+
ctx.inject(['assistantEvolution'], evolutionCtx => {
|
|
617
|
+
const producer = evolutionCtx.get('assistantEvolution');
|
|
618
|
+
if (!isTrustedDeliveryPresentationProducer(producer))
|
|
619
|
+
return;
|
|
620
|
+
return this.bindPresentationProducer('assistant-evolution', producer);
|
|
621
|
+
});
|
|
277
622
|
if (config.schedulerEnabled)
|
|
278
623
|
this.start();
|
|
279
624
|
ctx.effect(() => async () => {
|
|
280
625
|
this.active = false;
|
|
626
|
+
trustedDeliveryPreferenceProducers.delete(this);
|
|
627
|
+
this.preferenceFeedbackSink = undefined;
|
|
628
|
+
this.evaluationSink = undefined;
|
|
629
|
+
this.workflowTraceSink = undefined;
|
|
630
|
+
this.automationPresentationBinding?.dispose();
|
|
631
|
+
this.evolutionPresentationBinding?.dispose();
|
|
281
632
|
this.cancelModelSelectionWaiters();
|
|
282
633
|
for (const controller of this.toolApprovalControllers) {
|
|
283
634
|
controller.abort(new Error('assistant-delivery is stopping'));
|
|
@@ -743,16 +1094,26 @@ export class AssistantDeliveryService extends Service {
|
|
|
743
1094
|
}
|
|
744
1095
|
resolveDeadLetter(input) {
|
|
745
1096
|
this.assertActive();
|
|
746
|
-
|
|
1097
|
+
let operatorId;
|
|
1098
|
+
try {
|
|
1099
|
+
operatorId = canonicalLocalOperatorId(input.operatorId);
|
|
1100
|
+
}
|
|
1101
|
+
catch {
|
|
1102
|
+
throw new AssistantDeliveryError('runtime-conflict', 'dead-letter operator identity is invalid');
|
|
1103
|
+
}
|
|
1104
|
+
const operationDigest = createHash('sha256').update(JSON.stringify([
|
|
1105
|
+
operatorId, input.kind, input.id, input.expectedAttemptCount, input.resolution,
|
|
1106
|
+
])).digest('hex');
|
|
1107
|
+
const decision = this.policy.authorize({ subject: { kind: 'external', id: `local:${operatorId}` },
|
|
747
1108
|
action: 'delivery.resolve', resource: { kind: 'message', id: `${input.kind}:${input.id}` },
|
|
748
|
-
context: { initiator: 'foreground' } }, { idempotencyKey: `delivery-resolve:${
|
|
1109
|
+
context: { initiator: 'foreground' } }, { idempotencyKey: `delivery-resolve:${operationDigest}` });
|
|
749
1110
|
if (decision.effect !== 'allow')
|
|
750
1111
|
throw policyDenied(decision);
|
|
751
1112
|
return input.kind === 'inbox'
|
|
752
1113
|
? this.deliveryStore.resolveInbox({ inboxId: input.id, expectedAttemptCount: input.expectedAttemptCount,
|
|
753
|
-
resolution: input.resolution })
|
|
1114
|
+
resolution: input.resolution, operatorId })
|
|
754
1115
|
: this.deliveryStore.resolveOutbox({ outboxId: input.id, expectedAttemptCount: input.expectedAttemptCount,
|
|
755
|
-
resolution: input.resolution });
|
|
1116
|
+
resolution: input.resolution, operatorId });
|
|
756
1117
|
}
|
|
757
1118
|
registerInboundRuntime(runtime) {
|
|
758
1119
|
this.assertActive();
|
|
@@ -772,10 +1133,452 @@ export class AssistantDeliveryService extends Service {
|
|
|
772
1133
|
this.assertActive();
|
|
773
1134
|
return this.registry.register(adapter);
|
|
774
1135
|
}
|
|
1136
|
+
currentPreferenceTurn(agent) {
|
|
1137
|
+
this.assertActive();
|
|
1138
|
+
const attestation = this.preferenceTurns.get(agent);
|
|
1139
|
+
if (attestation === undefined
|
|
1140
|
+
|| String(agent.session.id) !== attestation.sessionId
|
|
1141
|
+
|| agent.session.header.cwd !== attestation.scope.workspace
|
|
1142
|
+
|| agent.session.header.agentPreset !== attestation.scope.preset)
|
|
1143
|
+
return undefined;
|
|
1144
|
+
const binding = this.deliveryStore.getBinding(attestation.bindingId);
|
|
1145
|
+
const inbox = this.deliveryStore.getInbox(attestation.sourceInboxId);
|
|
1146
|
+
const lineage = binding === undefined ? undefined : this.ownerLineageForBinding(binding);
|
|
1147
|
+
if (binding?.status !== 'active' || binding.version !== attestation.bindingVersion
|
|
1148
|
+
|| binding.sessionId !== attestation.sessionId
|
|
1149
|
+
|| inbox?.status !== 'claimed' || inbox.bindingId !== binding.id
|
|
1150
|
+
|| inbox.envelope.eventId !== attestation.sourceEventId
|
|
1151
|
+
|| lineage?.principalRecordId !== attestation.principalLineage.principalRecordId
|
|
1152
|
+
|| lineage?.principalVersion !== attestation.principalLineage.principalVersion
|
|
1153
|
+
|| !this.isOwnerFeedbackController(binding, inbox.envelope))
|
|
1154
|
+
return undefined;
|
|
1155
|
+
const started = agent.session.events.some(event => event.type === 'turn/start' && event.data.turn === attestation.turn);
|
|
1156
|
+
const ended = agent.session.events.some(event => event.type === 'turn/end' && event.data.turn === attestation.turn);
|
|
1157
|
+
return started && !ended ? attestation : undefined;
|
|
1158
|
+
}
|
|
1159
|
+
preferencePrincipalForAgent(agent) {
|
|
1160
|
+
this.assertActive();
|
|
1161
|
+
const sessionId = String(agent.session.id);
|
|
1162
|
+
const resolved = this.deliveryStore.getPreferencePrincipalForSession({
|
|
1163
|
+
sessionId, workspace: agent.session.header.cwd, preset: agent.session.header.agentPreset,
|
|
1164
|
+
});
|
|
1165
|
+
if (resolved === undefined)
|
|
1166
|
+
return undefined;
|
|
1167
|
+
const { binding, principal } = resolved;
|
|
1168
|
+
return Object.freeze({
|
|
1169
|
+
scope: Object.freeze({ workspace: binding.workspace, preset: binding.agentPreset }),
|
|
1170
|
+
principalId: externalPrincipalId(binding.principal),
|
|
1171
|
+
principalLineage: Object.freeze({
|
|
1172
|
+
principalRecordId: principal.id,
|
|
1173
|
+
principalVersion: principal.version,
|
|
1174
|
+
}),
|
|
1175
|
+
bindingId: binding.id,
|
|
1176
|
+
bindingVersion: binding.version,
|
|
1177
|
+
bindingGeneration: binding.generation,
|
|
1178
|
+
sessionId,
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
capturePreferenceTurn(agent, sourceInput, turnInput) {
|
|
1182
|
+
if (!this.active || typeof sourceInput !== 'object' || sourceInput === null
|
|
1183
|
+
|| !Number.isSafeInteger(turnInput) || turnInput < 0)
|
|
1184
|
+
return;
|
|
1185
|
+
const source = sourceInput;
|
|
1186
|
+
if (source.kind !== 'delivery' || typeof source.channel !== 'string'
|
|
1187
|
+
|| typeof source.account !== 'string' || typeof source.eventId !== 'string')
|
|
1188
|
+
return;
|
|
1189
|
+
const sessionId = String(agent.session.id);
|
|
1190
|
+
const binding = this.deliveryStore.getBindingBySession(sessionId);
|
|
1191
|
+
const inbox = this.deliveryStore.getInboxByProviderEvent(source.channel, source.account, source.eventId);
|
|
1192
|
+
const lineage = binding === undefined ? undefined : this.ownerLineageForBinding(binding);
|
|
1193
|
+
if (binding?.status !== 'active' || inbox?.status !== 'claimed' || inbox.bindingId !== binding.id
|
|
1194
|
+
|| binding.sessionId !== sessionId || binding.workspace !== agent.session.header.cwd
|
|
1195
|
+
|| binding.agentPreset !== agent.session.header.agentPreset
|
|
1196
|
+
|| lineage === undefined
|
|
1197
|
+
|| !this.isOwnerFeedbackController(binding, inbox.envelope))
|
|
1198
|
+
return;
|
|
1199
|
+
this.preferenceTurns.set(agent, Object.freeze({
|
|
1200
|
+
scope: Object.freeze({ workspace: binding.workspace, preset: binding.agentPreset }),
|
|
1201
|
+
principalId: externalPrincipalId(binding.principal),
|
|
1202
|
+
principalLineage: lineage,
|
|
1203
|
+
bindingId: binding.id,
|
|
1204
|
+
bindingVersion: binding.version,
|
|
1205
|
+
sessionId,
|
|
1206
|
+
sourceEventId: source.eventId,
|
|
1207
|
+
sourceInboxId: inbox.id,
|
|
1208
|
+
turn: turnInput,
|
|
1209
|
+
}));
|
|
1210
|
+
}
|
|
1211
|
+
trustedPreferenceProducerGeneration() {
|
|
1212
|
+
this.assertActive();
|
|
1213
|
+
return this.preferenceProducerGeneration;
|
|
1214
|
+
}
|
|
1215
|
+
/** Exact process-local Preference owner registration; copied or self-asserted sinks fail. */
|
|
1216
|
+
registerTrustedPreferenceSink(registration) {
|
|
1217
|
+
this.assertActive();
|
|
1218
|
+
if (registration.protocol !== DELIVERY_PREFERENCE_PROJECTION_PROTOCOL
|
|
1219
|
+
|| registration.producer !== 'preference-learning'
|
|
1220
|
+
|| registration.generation !== this.preferenceProducerGeneration
|
|
1221
|
+
|| typeof registration.append !== 'function'
|
|
1222
|
+
|| (registration.appendSynchronously !== undefined
|
|
1223
|
+
&& typeof registration.appendSynchronously !== 'function')
|
|
1224
|
+
|| !registrationOwnedByPreference(registration)) {
|
|
1225
|
+
throw new AssistantDeliveryError('runtime-conflict', 'trusted Preference registration is invalid');
|
|
1226
|
+
}
|
|
1227
|
+
if (this.preferenceFeedbackSink !== undefined) {
|
|
1228
|
+
if (this.preferenceFeedbackSink.registration === registration) {
|
|
1229
|
+
return this.preferenceFeedbackSink.dispose;
|
|
1230
|
+
}
|
|
1231
|
+
throw new AssistantDeliveryError('runtime-conflict', 'trusted Preference sink is already registered');
|
|
1232
|
+
}
|
|
1233
|
+
const token = Symbol('assistant-delivery.trusted-preference');
|
|
1234
|
+
let active = true;
|
|
1235
|
+
const dispose = () => {
|
|
1236
|
+
if (!active)
|
|
1237
|
+
return;
|
|
1238
|
+
active = false;
|
|
1239
|
+
if (this.preferenceFeedbackSink?.token === token)
|
|
1240
|
+
this.preferenceFeedbackSink = undefined;
|
|
1241
|
+
};
|
|
1242
|
+
this.preferenceFeedbackSink = Object.freeze({ token, registration, dispose });
|
|
1243
|
+
this.deliveryStore.requeuePreferenceProjections();
|
|
1244
|
+
void this.drainPreferenceProjections();
|
|
1245
|
+
return dispose;
|
|
1246
|
+
}
|
|
1247
|
+
trustedEvaluationProducerGeneration() {
|
|
1248
|
+
this.assertActive();
|
|
1249
|
+
return this.evaluationProducerGeneration;
|
|
1250
|
+
}
|
|
1251
|
+
registerTrustedDeliveryEvaluationSink(registration) {
|
|
1252
|
+
this.assertActive();
|
|
1253
|
+
if (registration.protocol !== TRUSTED_EVALUATION_PRODUCER_PROTOCOL
|
|
1254
|
+
|| registration.producer !== 'assistant-delivery'
|
|
1255
|
+
|| registration.generation !== this.evaluationProducerGeneration
|
|
1256
|
+
|| typeof registration.issueCapability !== 'function'
|
|
1257
|
+
|| typeof registration.append !== 'function'
|
|
1258
|
+
|| !registrationOwnedByEvaluation(registration)) {
|
|
1259
|
+
throw new AssistantDeliveryError('runtime-conflict', 'trusted Evaluation registration is invalid');
|
|
1260
|
+
}
|
|
1261
|
+
if (this.evaluationSink !== undefined) {
|
|
1262
|
+
throw new AssistantDeliveryError('runtime-conflict', 'trusted Evaluation sink is already registered');
|
|
1263
|
+
}
|
|
1264
|
+
const token = Symbol('assistant-delivery.trusted-evaluation');
|
|
1265
|
+
this.evaluationSink = Object.freeze({ token, registration });
|
|
1266
|
+
let active = true;
|
|
1267
|
+
return () => {
|
|
1268
|
+
if (!active)
|
|
1269
|
+
return;
|
|
1270
|
+
active = false;
|
|
1271
|
+
if (this.evaluationSink?.token === token)
|
|
1272
|
+
this.evaluationSink = undefined;
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
/**
|
|
1276
|
+
* Register Growth's exact private sink. Replaying current is deliberately
|
|
1277
|
+
* queued after this method returns so a registration receipt can never be
|
|
1278
|
+
* confused with synchronous projection success.
|
|
1279
|
+
*/
|
|
1280
|
+
registerWorkflowTraceSink(input) {
|
|
1281
|
+
this.assertActive();
|
|
1282
|
+
if (typeof input !== 'object' || input === null
|
|
1283
|
+
|| Object.keys(input).sort().join(',') !== 'contractVersion,sink'
|
|
1284
|
+
|| input.contractVersion !== ASSISTANT_GROWTH_CONTRACT_VERSION
|
|
1285
|
+
|| typeof input.sink !== 'object' || input.sink === null
|
|
1286
|
+
|| typeof input.sink.projectWorkflowTraceRevision !== 'function') {
|
|
1287
|
+
throw new AssistantDeliveryError('runtime-conflict', 'workflow trace sink registration is invalid');
|
|
1288
|
+
}
|
|
1289
|
+
if (this.workflowTraceSink !== undefined) {
|
|
1290
|
+
throw new AssistantDeliveryError('runtime-conflict', 'workflow trace sink is already registered');
|
|
1291
|
+
}
|
|
1292
|
+
const token = Symbol('assistant-delivery.workflow-trace');
|
|
1293
|
+
this.workflowTraceSink = Object.freeze({ token, sink: input.sink });
|
|
1294
|
+
const source = this.deliveryStore.workflowTraceSourceAttestation();
|
|
1295
|
+
let registered = true;
|
|
1296
|
+
const receipt = Object.freeze({
|
|
1297
|
+
contractVersion: ASSISTANT_GROWTH_CONTRACT_VERSION,
|
|
1298
|
+
...source,
|
|
1299
|
+
dispose: () => {
|
|
1300
|
+
if (!registered)
|
|
1301
|
+
return;
|
|
1302
|
+
registered = false;
|
|
1303
|
+
if (this.workflowTraceSink?.token === token)
|
|
1304
|
+
this.workflowTraceSink = undefined;
|
|
1305
|
+
},
|
|
1306
|
+
});
|
|
1307
|
+
queueMicrotask(() => {
|
|
1308
|
+
if (!this.active || this.workflowTraceSink?.token !== token)
|
|
1309
|
+
return;
|
|
1310
|
+
this.deliveryStore.requeueCurrentWorkflowTraces();
|
|
1311
|
+
void this.drainWorkflowTraces();
|
|
1312
|
+
});
|
|
1313
|
+
return receipt;
|
|
1314
|
+
}
|
|
1315
|
+
resolveWorkflowAutomationTemplate(input) {
|
|
1316
|
+
this.assertActive();
|
|
1317
|
+
if (typeof input !== 'object' || input === null
|
|
1318
|
+
|| Object.keys(input).sort().join(',') !== 'contractVersion,ownerBindingId,scope,template'
|
|
1319
|
+
|| input.contractVersion !== ASSISTANT_GROWTH_CONTRACT_VERSION
|
|
1320
|
+
|| typeof input.ownerBindingId !== 'string') {
|
|
1321
|
+
throw new AssistantDeliveryError('runtime-conflict', 'workflow template resolution request is invalid');
|
|
1322
|
+
}
|
|
1323
|
+
const template = validateWorkflowAutomationTemplate(input.template);
|
|
1324
|
+
const scope = validateWorkflowScope(input.scope);
|
|
1325
|
+
const stored = this.deliveryStore.getWorkflowAutomationTemplate(template);
|
|
1326
|
+
const binding = this.deliveryStore.getBinding(input.ownerBindingId);
|
|
1327
|
+
const owner = binding === undefined ? undefined : this.deliveryStore.getPrincipal(binding.principal);
|
|
1328
|
+
const principalId = binding === undefined ? undefined : externalPrincipalId(binding.principal);
|
|
1329
|
+
if (stored === undefined || stored.status !== 'active'
|
|
1330
|
+
|| stored.resolved.ownerBindingId !== input.ownerBindingId
|
|
1331
|
+
|| stored.resolved.scope.workspace !== scope.workspace
|
|
1332
|
+
|| stored.resolved.scope.preset !== scope.preset
|
|
1333
|
+
|| binding?.status !== 'active' || binding.workspace !== scope.workspace
|
|
1334
|
+
|| binding.agentPreset !== scope.preset || owner?.status !== 'active' || owner.role !== 'owner'
|
|
1335
|
+
|| stored.review.bindingId !== binding.id
|
|
1336
|
+
|| stored.review.bindingVersion !== binding.version
|
|
1337
|
+
|| stored.review.bindingGeneration !== binding.generation
|
|
1338
|
+
|| stored.review.principalId !== owner.id
|
|
1339
|
+
|| stored.resolved.principalId !== principalId) {
|
|
1340
|
+
throw new AssistantDeliveryError('missing-binding', 'workflow template owner review is stale');
|
|
1341
|
+
}
|
|
1342
|
+
const decision = this.policy.authorize({
|
|
1343
|
+
subject: { kind: 'background', id: 'assistant-growth-experiments',
|
|
1344
|
+
workspace: scope.workspace, principal: externalPrincipalId(binding.principal) },
|
|
1345
|
+
action: 'inspect',
|
|
1346
|
+
resource: { kind: 'evolution', id: `workflow-template:${template.templateRef}` },
|
|
1347
|
+
context: { initiator: 'background' },
|
|
1348
|
+
}, { idempotencyKey: `workflow-template-resolve:${template.templateDigest}:${binding.version}` });
|
|
1349
|
+
if (decision.effect !== 'allow')
|
|
1350
|
+
throw policyDenied(decision);
|
|
1351
|
+
// Policy evaluation is not a lock. Re-read the mutable route immediately
|
|
1352
|
+
// before returning private prompt material.
|
|
1353
|
+
const currentBinding = this.deliveryStore.getBinding(binding.id);
|
|
1354
|
+
const currentOwner = currentBinding === undefined
|
|
1355
|
+
? undefined
|
|
1356
|
+
: this.deliveryStore.getPrincipal(currentBinding.principal);
|
|
1357
|
+
const currentTemplate = this.deliveryStore.getWorkflowAutomationTemplate(template);
|
|
1358
|
+
if (currentBinding?.status !== 'active' || currentBinding.version !== binding.version
|
|
1359
|
+
|| currentBinding.generation !== binding.generation
|
|
1360
|
+
|| currentOwner?.status !== 'active' || currentOwner.role !== 'owner'
|
|
1361
|
+
|| currentTemplate?.status !== 'active' || currentTemplate.version !== stored.version
|
|
1362
|
+
|| currentTemplate.review.bindingId !== currentBinding.id
|
|
1363
|
+
|| currentTemplate.review.bindingVersion !== currentBinding.version
|
|
1364
|
+
|| currentTemplate.review.bindingGeneration !== currentBinding.generation
|
|
1365
|
+
|| currentTemplate.review.principalId !== currentOwner.id
|
|
1366
|
+
|| currentTemplate.resolved.principalId !== externalPrincipalId(currentBinding.principal)) {
|
|
1367
|
+
throw new AssistantDeliveryError('missing-binding', 'workflow template authority changed during resolution');
|
|
1368
|
+
}
|
|
1369
|
+
return validateResolvedWorkflowAutomationTemplate(currentTemplate.resolved);
|
|
1370
|
+
}
|
|
1371
|
+
/**
|
|
1372
|
+
* Host-only bridge from one exact reviewed workflow template to a v2 Policy
|
|
1373
|
+
* approval route. This method is not registered as an Agent tool.
|
|
1374
|
+
*/
|
|
1375
|
+
prepareWorkflowApproval(input) {
|
|
1376
|
+
this.assertActive();
|
|
1377
|
+
if (typeof input !== 'object' || input === null
|
|
1378
|
+
|| Object.keys(input).sort().join(',') !== 'contractVersion,ownerBindingId,scope,sourceId,template') {
|
|
1379
|
+
throw new AssistantDeliveryError('runtime-conflict', 'workflow approval route request is invalid');
|
|
1380
|
+
}
|
|
1381
|
+
const sourceId = canonicalBackgroundSourceId(input.sourceId);
|
|
1382
|
+
const resolved = this.resolveWorkflowAutomationTemplate({
|
|
1383
|
+
contractVersion: input.contractVersion,
|
|
1384
|
+
template: input.template,
|
|
1385
|
+
scope: input.scope,
|
|
1386
|
+
ownerBindingId: input.ownerBindingId,
|
|
1387
|
+
});
|
|
1388
|
+
const route = this.deliveryStore.resolveApprovalRouteByBinding({
|
|
1389
|
+
sourceId,
|
|
1390
|
+
bindingId: resolved.ownerBindingId,
|
|
1391
|
+
scope: resolved.scope,
|
|
1392
|
+
principalId: resolved.principalId,
|
|
1393
|
+
});
|
|
1394
|
+
if (route === undefined) {
|
|
1395
|
+
throw new AssistantDeliveryError('missing-binding', 'workflow approval owner route changed');
|
|
1396
|
+
}
|
|
1397
|
+
return route;
|
|
1398
|
+
}
|
|
1399
|
+
/**
|
|
1400
|
+
* Host-only, content-free route minting seam for Preference promotion.
|
|
1401
|
+
* No binding id is accepted from the caller; Delivery resolves the unique
|
|
1402
|
+
* current binding from the exact owner lineage and generation.
|
|
1403
|
+
*/
|
|
1404
|
+
prepareOwnerApprovalForPreference(input) {
|
|
1405
|
+
this.assertActive();
|
|
1406
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input)
|
|
1407
|
+
|| Object.keys(input).sort().join(',')
|
|
1408
|
+
!== 'ownerGeneration,principalId,principalLineage,scope,sourceId'
|
|
1409
|
+
|| typeof input.scope !== 'object' || input.scope === null || Array.isArray(input.scope)
|
|
1410
|
+
|| Object.keys(input.scope).sort().join(',') !== 'preset,workspace'
|
|
1411
|
+
|| typeof input.principalLineage !== 'object' || input.principalLineage === null
|
|
1412
|
+
|| Array.isArray(input.principalLineage)
|
|
1413
|
+
|| Object.keys(input.principalLineage).sort().join(',') !== 'principalRecordId,principalVersion') {
|
|
1414
|
+
throw new AssistantDeliveryError('runtime-conflict', 'preference approval owner authority is invalid');
|
|
1415
|
+
}
|
|
1416
|
+
try {
|
|
1417
|
+
const route = this.deliveryStore.resolveOwnerApprovalForPreference(input);
|
|
1418
|
+
return route ?? Object.freeze({ kind: 'stale-owner' });
|
|
1419
|
+
}
|
|
1420
|
+
catch (error) {
|
|
1421
|
+
if (error instanceof DeliveryStoreError && error.code === 'invalid-binding') {
|
|
1422
|
+
throw new AssistantDeliveryError('runtime-conflict', 'preference approval owner authority is invalid');
|
|
1423
|
+
}
|
|
1424
|
+
throw error;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
775
1427
|
health() {
|
|
776
1428
|
this.assertActive();
|
|
777
1429
|
return { ...this.deliveryStore.health(), adapters: this.registry.size() };
|
|
778
1430
|
}
|
|
1431
|
+
/** Exact process-local proof for a Delivery-minted publisher registration. */
|
|
1432
|
+
ownsTrustedDeliveryPresentationRegistration(registration) {
|
|
1433
|
+
return this.active && typeof registration === 'object' && registration !== null
|
|
1434
|
+
&& this.activePresentationRegistrations.has(registration);
|
|
1435
|
+
}
|
|
1436
|
+
getDeliveryPresentation(presentationKey) {
|
|
1437
|
+
this.assertActive();
|
|
1438
|
+
return this.deliveryStore.getDeliveryPresentation(presentationKey);
|
|
1439
|
+
}
|
|
1440
|
+
presentationBinding(producerId) {
|
|
1441
|
+
return producerId === 'assistant-automations'
|
|
1442
|
+
? this.automationPresentationBinding
|
|
1443
|
+
: this.evolutionPresentationBinding;
|
|
1444
|
+
}
|
|
1445
|
+
setPresentationBinding(producerId, binding) {
|
|
1446
|
+
if (producerId === 'assistant-automations')
|
|
1447
|
+
this.automationPresentationBinding = binding;
|
|
1448
|
+
else
|
|
1449
|
+
this.evolutionPresentationBinding = binding;
|
|
1450
|
+
}
|
|
1451
|
+
presentationProducerGeneration(producer) {
|
|
1452
|
+
let generation;
|
|
1453
|
+
try {
|
|
1454
|
+
generation = producer.trustedDeliveryPresentationProducerGeneration();
|
|
1455
|
+
}
|
|
1456
|
+
catch {
|
|
1457
|
+
throw new AssistantDeliveryError('runtime-conflict', 'presentation producer generation is unavailable');
|
|
1458
|
+
}
|
|
1459
|
+
if (typeof generation !== 'string') {
|
|
1460
|
+
throw new AssistantDeliveryError('runtime-conflict', 'presentation producer generation is invalid');
|
|
1461
|
+
}
|
|
1462
|
+
const normalized = generation.normalize('NFC').trim();
|
|
1463
|
+
const hasControl = [...normalized].some(character => {
|
|
1464
|
+
const code = character.codePointAt(0);
|
|
1465
|
+
return code <= 0x1f || code === 0x7f;
|
|
1466
|
+
});
|
|
1467
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/u.test(normalized) || hasControl) {
|
|
1468
|
+
throw new AssistantDeliveryError('runtime-conflict', 'presentation producer generation is invalid');
|
|
1469
|
+
}
|
|
1470
|
+
return normalized;
|
|
1471
|
+
}
|
|
1472
|
+
/**
|
|
1473
|
+
* Delivery owns the registration and its publish closure. The producer only
|
|
1474
|
+
* receives that closure after it has proven this exact in-process object is
|
|
1475
|
+
* its expected Delivery owner; replacement and disposal revoke it eagerly.
|
|
1476
|
+
*/
|
|
1477
|
+
bindPresentationProducer(producerId, producer) {
|
|
1478
|
+
const generation = this.presentationProducerGeneration(producer);
|
|
1479
|
+
const current = this.presentationBinding(producerId);
|
|
1480
|
+
// Cordis can expose more than one proxy object for the same service
|
|
1481
|
+
// instance. A producer generation is minted once per concrete producer
|
|
1482
|
+
// lifetime, so it is the stable instance identity across those proxies.
|
|
1483
|
+
// Do not issue a second publisher merely because a later inject callback
|
|
1484
|
+
// reached the same producer through a different proxy.
|
|
1485
|
+
if (current?.generation === generation)
|
|
1486
|
+
return current.dispose;
|
|
1487
|
+
let live = true;
|
|
1488
|
+
let registration;
|
|
1489
|
+
registration = Object.freeze({
|
|
1490
|
+
protocol: TRUSTED_DELIVERY_PRESENTATION_PRODUCER_PROTOCOL,
|
|
1491
|
+
producer: producerId,
|
|
1492
|
+
generation,
|
|
1493
|
+
owner: this,
|
|
1494
|
+
publish: (input) => {
|
|
1495
|
+
this.assertCurrentPresentationProducer(producerId, producer, generation, registration, live);
|
|
1496
|
+
return this.publishTrustedDeliveryPresentation(producerId, input);
|
|
1497
|
+
},
|
|
1498
|
+
});
|
|
1499
|
+
this.activePresentationRegistrations.add(registration);
|
|
1500
|
+
let unregister;
|
|
1501
|
+
let binding;
|
|
1502
|
+
const dispose = () => {
|
|
1503
|
+
if (!live)
|
|
1504
|
+
return;
|
|
1505
|
+
live = false;
|
|
1506
|
+
this.activePresentationRegistrations.delete(registration);
|
|
1507
|
+
if (this.presentationBinding(producerId) === binding) {
|
|
1508
|
+
this.setPresentationBinding(producerId, undefined);
|
|
1509
|
+
}
|
|
1510
|
+
unregister?.();
|
|
1511
|
+
};
|
|
1512
|
+
binding = Object.freeze({ producerId, producer, generation, registration, dispose });
|
|
1513
|
+
this.setPresentationBinding(producerId, binding);
|
|
1514
|
+
try {
|
|
1515
|
+
unregister = producer.registerTrustedDeliveryPresentationSink(registration);
|
|
1516
|
+
if (typeof unregister !== 'function') {
|
|
1517
|
+
throw new AssistantDeliveryError('runtime-conflict', 'presentation producer returned no registration disposer');
|
|
1518
|
+
}
|
|
1519
|
+
// A reload that swaps its generation while registering cannot inherit a
|
|
1520
|
+
// publisher issued to the preceding instance state.
|
|
1521
|
+
if (this.presentationProducerGeneration(producer) !== generation) {
|
|
1522
|
+
throw new AssistantDeliveryError('runtime-conflict', 'presentation producer changed during registration');
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
catch (error) {
|
|
1526
|
+
live = false;
|
|
1527
|
+
this.activePresentationRegistrations.delete(registration);
|
|
1528
|
+
if (this.presentationBinding(producerId) === binding) {
|
|
1529
|
+
this.setPresentationBinding(producerId, current);
|
|
1530
|
+
}
|
|
1531
|
+
try {
|
|
1532
|
+
unregister?.();
|
|
1533
|
+
}
|
|
1534
|
+
catch {
|
|
1535
|
+
// Registration never became live, so cleanup cannot replace the
|
|
1536
|
+
// original registration failure.
|
|
1537
|
+
}
|
|
1538
|
+
throw error;
|
|
1539
|
+
}
|
|
1540
|
+
// Do not revoke the old producer until the replacement has accepted the
|
|
1541
|
+
// new publisher; a reentrant durable outbox drain then has one valid owner.
|
|
1542
|
+
current?.dispose();
|
|
1543
|
+
return dispose;
|
|
1544
|
+
}
|
|
1545
|
+
assertCurrentPresentationProducer(producerId, producer, generation, registration, live) {
|
|
1546
|
+
this.assertActive();
|
|
1547
|
+
const current = this.presentationBinding(producerId);
|
|
1548
|
+
if (!live || !this.activePresentationRegistrations.has(registration)
|
|
1549
|
+
|| current?.producer !== producer || current.generation !== generation
|
|
1550
|
+
|| current.registration !== registration
|
|
1551
|
+
|| this.presentationProducerGeneration(producer) !== generation) {
|
|
1552
|
+
throw new AssistantDeliveryError('runtime-conflict', 'stale trusted presentation producer capability');
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
publishTrustedDeliveryPresentation(producerId, input) {
|
|
1556
|
+
this.assertActive();
|
|
1557
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input)
|
|
1558
|
+
|| typeof input.presentation !== 'object'
|
|
1559
|
+
|| input.presentation === null
|
|
1560
|
+
|| Array.isArray(input.presentation)) {
|
|
1561
|
+
throw new AssistantDeliveryError('runtime-conflict', 'trusted presentation input is invalid');
|
|
1562
|
+
}
|
|
1563
|
+
const exactApproval = producerId === 'assistant-evolution'
|
|
1564
|
+
&& input.presentation.kind === 'approval-application'
|
|
1565
|
+
&& input.presentationKey === `approval-application:${input.presentation.policyProposalId}`
|
|
1566
|
+
&& input.originalOutboxIdempotencyKey === `approval-card:${input.presentation.policyProposalId}`;
|
|
1567
|
+
const incidentKey = input.presentation.kind === 'automation-incident'
|
|
1568
|
+
? `automation-incident:${input.presentation.incidentId}:g${input.presentation.lifecycleGeneration}`
|
|
1569
|
+
: undefined;
|
|
1570
|
+
const exactIncident = producerId === 'assistant-automations'
|
|
1571
|
+
&& input.presentation.kind === 'automation-incident'
|
|
1572
|
+
&& input.presentationKey === incidentKey
|
|
1573
|
+
&& input.originalOutboxIdempotencyKey === incidentKey
|
|
1574
|
+
&& input.revision === input.presentation.incidentRevision;
|
|
1575
|
+
if (!exactApproval && !exactIncident) {
|
|
1576
|
+
throw new AssistantDeliveryError('runtime-conflict', 'presentation does not target its exact durable provider message');
|
|
1577
|
+
}
|
|
1578
|
+
const stored = this.deliveryStore.publishDeliveryPresentation(input);
|
|
1579
|
+
void this.drainDeliveryPresentations();
|
|
1580
|
+
return stored;
|
|
1581
|
+
}
|
|
779
1582
|
isInboundAuthorized(binding, envelope) {
|
|
780
1583
|
if (!this.active)
|
|
781
1584
|
return false;
|
|
@@ -824,6 +1627,547 @@ export class AssistantDeliveryService extends Service {
|
|
|
824
1627
|
context: { initiator: 'external' },
|
|
825
1628
|
}).effect === 'allow';
|
|
826
1629
|
}
|
|
1630
|
+
isOwnerFeedbackController(binding, envelope) {
|
|
1631
|
+
if (!this.isInboundAuthorized(binding, envelope))
|
|
1632
|
+
return false;
|
|
1633
|
+
const principal = this.deliveryStore.getPrincipal(binding.principal);
|
|
1634
|
+
const inbox = this.deliveryStore.getInboxByProviderEvent(envelope.channel, envelope.account, envelope.eventId);
|
|
1635
|
+
if (principal?.status !== 'active' || principal.role !== 'owner'
|
|
1636
|
+
|| inbox?.status !== 'claimed'
|
|
1637
|
+
|| inbox.bindingId !== binding.id
|
|
1638
|
+
|| JSON.stringify(inbox.envelope) !== JSON.stringify(envelope)) {
|
|
1639
|
+
return false;
|
|
1640
|
+
}
|
|
1641
|
+
return this.policy.evaluate({
|
|
1642
|
+
subject: {
|
|
1643
|
+
kind: 'agent',
|
|
1644
|
+
id: binding.agentPreset,
|
|
1645
|
+
workspace: binding.workspace,
|
|
1646
|
+
principal: externalPrincipalId(binding.principal),
|
|
1647
|
+
},
|
|
1648
|
+
action: 'reply',
|
|
1649
|
+
resource: { kind: 'message', id: binding.id },
|
|
1650
|
+
context: { initiator: 'external' },
|
|
1651
|
+
}).effect === 'allow';
|
|
1652
|
+
}
|
|
1653
|
+
ownerLineageForBinding(binding) {
|
|
1654
|
+
const principal = this.deliveryStore.getPrincipal(binding.principal);
|
|
1655
|
+
if (principal?.status !== 'active' || principal.role !== 'owner'
|
|
1656
|
+
|| JSON.stringify(principal.principal) !== JSON.stringify(binding.principal))
|
|
1657
|
+
return undefined;
|
|
1658
|
+
return Object.freeze({
|
|
1659
|
+
principalRecordId: principal.id,
|
|
1660
|
+
principalVersion: principal.version,
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
authorizeOwnerPreferenceFeedback(binding, envelope, selections) {
|
|
1664
|
+
if (!this.isOwnerFeedbackController(binding, envelope)
|
|
1665
|
+
|| !Array.isArray(selections) || selections.length < 1 || selections.length > 16)
|
|
1666
|
+
return undefined;
|
|
1667
|
+
const inbox = this.deliveryStore.getInboxByProviderEvent(envelope.channel, envelope.account, envelope.eventId);
|
|
1668
|
+
if (inbox?.status !== 'claimed' || inbox.bindingId !== binding.id
|
|
1669
|
+
|| JSON.stringify(inbox.envelope) !== JSON.stringify(envelope))
|
|
1670
|
+
return undefined;
|
|
1671
|
+
const principalLineage = this.ownerLineageForBinding(binding);
|
|
1672
|
+
if (principalLineage === undefined)
|
|
1673
|
+
return undefined;
|
|
1674
|
+
const subject = {
|
|
1675
|
+
kind: 'external',
|
|
1676
|
+
id: externalPrincipalId(binding.principal),
|
|
1677
|
+
workspace: binding.workspace,
|
|
1678
|
+
};
|
|
1679
|
+
const requests = selections.map(selection => ({
|
|
1680
|
+
selection,
|
|
1681
|
+
authorization: {
|
|
1682
|
+
subject,
|
|
1683
|
+
action: 'signal',
|
|
1684
|
+
resource: { kind: 'preference', id: `${binding.agentPreset}/${selection.preferenceKey}` },
|
|
1685
|
+
context: { initiator: 'external' },
|
|
1686
|
+
},
|
|
1687
|
+
}));
|
|
1688
|
+
const denied = requests.find(entry => this.policy.evaluate(entry.authorization).effect !== 'allow');
|
|
1689
|
+
if (denied !== undefined) {
|
|
1690
|
+
this.policy.authorize(denied.authorization, {
|
|
1691
|
+
idempotencyKey: this.preferenceSignalAuthorizationKey(binding, envelope, denied.authorization.resource.id, denied.selection.candidateValue),
|
|
1692
|
+
auditDetails: { bindingVersion: binding.version },
|
|
1693
|
+
});
|
|
1694
|
+
return undefined;
|
|
1695
|
+
}
|
|
1696
|
+
for (const { authorization, selection } of requests) {
|
|
1697
|
+
const decision = this.policy.authorize(authorization, {
|
|
1698
|
+
idempotencyKey: this.preferenceSignalAuthorizationKey(binding, envelope, authorization.resource.id, selection.candidateValue),
|
|
1699
|
+
auditDetails: { bindingVersion: binding.version },
|
|
1700
|
+
});
|
|
1701
|
+
if (decision.effect !== 'allow')
|
|
1702
|
+
return undefined;
|
|
1703
|
+
}
|
|
1704
|
+
// Provider clocks are not authoritative. The locally persisted Inbox time
|
|
1705
|
+
// is bounded by the same Host clock as Preference Learning's retention gate.
|
|
1706
|
+
const replyTarget = envelope.metadata?.replyToProviderMessageId;
|
|
1707
|
+
const sourceOutbox = typeof replyTarget === 'string'
|
|
1708
|
+
? this.deliveryStore.getOutboxByProviderMessage(envelope.channel, envelope.account, replyTarget)
|
|
1709
|
+
: undefined;
|
|
1710
|
+
const sourceEventId = sourceOutbox?.intent.replyToEventId;
|
|
1711
|
+
const sourceInbox = sourceEventId === undefined
|
|
1712
|
+
? undefined
|
|
1713
|
+
: this.deliveryStore.getInboxByProviderEvent(envelope.channel, envelope.account, sourceEventId);
|
|
1714
|
+
const exposureTarget = sourceOutbox !== undefined
|
|
1715
|
+
&& sourceOutbox.providerMessageId === replyTarget
|
|
1716
|
+
&& sourceOutbox.intent.bindingId === binding.id
|
|
1717
|
+
&& sourceInbox?.status === 'processed'
|
|
1718
|
+
&& sourceInbox.bindingId === binding.id
|
|
1719
|
+
&& JSON.stringify(sourceOutbox.intent.target.conversation) === JSON.stringify(binding.conversation)
|
|
1720
|
+
&& JSON.stringify(sourceOutbox.intent.target.principal) === JSON.stringify(binding.principal)
|
|
1721
|
+
? Object.freeze({ sourceInboxId: sourceInbox.id, sourceOutboxId: sourceOutbox.id })
|
|
1722
|
+
: undefined;
|
|
1723
|
+
return Object.freeze({
|
|
1724
|
+
occurredAt: inbox.receivedAt,
|
|
1725
|
+
principalLineage,
|
|
1726
|
+
admissionCursor: inbox.admissionCursor,
|
|
1727
|
+
...(exposureTarget === undefined ? {} : { exposureTarget }),
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1730
|
+
preferenceSignalAuthorizationKey(binding, envelope, resourceId, candidateValue) {
|
|
1731
|
+
const digest = createHash('sha256').update('assistant-delivery-preference-authorization-v2\0')
|
|
1732
|
+
.update(JSON.stringify([
|
|
1733
|
+
binding.id,
|
|
1734
|
+
binding.version,
|
|
1735
|
+
envelope.channel,
|
|
1736
|
+
envelope.account,
|
|
1737
|
+
envelope.eventId,
|
|
1738
|
+
resourceId,
|
|
1739
|
+
candidateValue,
|
|
1740
|
+
]))
|
|
1741
|
+
.digest('hex');
|
|
1742
|
+
return `delivery-pref-auth:${digest}`;
|
|
1743
|
+
}
|
|
1744
|
+
async dispatchPreferenceFeedback(events) {
|
|
1745
|
+
this.deliveryStore.enqueuePreferenceProjection(events);
|
|
1746
|
+
// Preserve the Inbox admission order through the process-local sink. A
|
|
1747
|
+
// later owner control must not overtake an earlier projection from another
|
|
1748
|
+
// binding of the same owner scope.
|
|
1749
|
+
await this.drainPreferenceProjections();
|
|
1750
|
+
return 'recorded';
|
|
1751
|
+
}
|
|
1752
|
+
async dispatchLearningCommand(binding, envelope, action, preferenceKey) {
|
|
1753
|
+
if (!this.isOwnerFeedbackController(binding, envelope))
|
|
1754
|
+
return 'forbidden';
|
|
1755
|
+
const inbox = this.deliveryStore.getInboxByProviderEvent(envelope.channel, envelope.account, envelope.eventId);
|
|
1756
|
+
if (inbox?.status !== 'claimed' || inbox.bindingId !== binding.id
|
|
1757
|
+
|| JSON.stringify(inbox.envelope) !== JSON.stringify(envelope))
|
|
1758
|
+
return 'forbidden';
|
|
1759
|
+
const principalLineage = this.ownerLineageForBinding(binding);
|
|
1760
|
+
if (principalLineage === undefined)
|
|
1761
|
+
return 'forbidden';
|
|
1762
|
+
const sink = this.preferenceFeedbackSink;
|
|
1763
|
+
if (sink === undefined
|
|
1764
|
+
|| sink.registration.generation !== this.preferenceProducerGeneration
|
|
1765
|
+
|| !registrationOwnedByPreference(sink.registration)
|
|
1766
|
+
|| typeof sink.registration.control !== 'function')
|
|
1767
|
+
return 'unavailable';
|
|
1768
|
+
const idempotencyKey = `delivery-learning-control-v2:${createHash('sha256')
|
|
1769
|
+
.update('assistant-delivery-learning-control-v2\0')
|
|
1770
|
+
.update(JSON.stringify([
|
|
1771
|
+
binding.id,
|
|
1772
|
+
binding.version,
|
|
1773
|
+
binding.sessionId,
|
|
1774
|
+
principalLineage.principalRecordId,
|
|
1775
|
+
principalLineage.principalVersion,
|
|
1776
|
+
inbox.admissionCursor.epoch,
|
|
1777
|
+
inbox.admissionCursor.sequence,
|
|
1778
|
+
envelope.channel,
|
|
1779
|
+
envelope.account,
|
|
1780
|
+
envelope.eventId,
|
|
1781
|
+
action,
|
|
1782
|
+
preferenceKey ?? null,
|
|
1783
|
+
]))
|
|
1784
|
+
.digest('hex')}`;
|
|
1785
|
+
const request = Object.freeze({
|
|
1786
|
+
scope: Object.freeze({ workspace: binding.workspace, preset: binding.agentPreset }),
|
|
1787
|
+
principalId: externalPrincipalId(binding.principal),
|
|
1788
|
+
principalLineage,
|
|
1789
|
+
admissionCursor: inbox.admissionCursor,
|
|
1790
|
+
action,
|
|
1791
|
+
...(preferenceKey === undefined ? {} : { preferenceKey }),
|
|
1792
|
+
occurredAt: inbox.receivedAt,
|
|
1793
|
+
idempotencyKey,
|
|
1794
|
+
});
|
|
1795
|
+
await this.drainPreferenceProjections();
|
|
1796
|
+
if (this.deliveryStore.hasBlockingPreferenceProjectionBefore(request))
|
|
1797
|
+
return 'unknown';
|
|
1798
|
+
try {
|
|
1799
|
+
const receipt = await sink.registration.control(request);
|
|
1800
|
+
validateLearningControlReceipt(receipt, request);
|
|
1801
|
+
return receipt;
|
|
1802
|
+
}
|
|
1803
|
+
catch {
|
|
1804
|
+
return 'unknown';
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
completedPreferenceEvents(binding, envelope, reply) {
|
|
1808
|
+
const directive = envelope.kind === 'text'
|
|
1809
|
+
? classifyNaturalPreferenceDirective(envelope.text)
|
|
1810
|
+
: undefined;
|
|
1811
|
+
const naturalSelection = directive?.kind === 'durable-exact-selection'
|
|
1812
|
+
? directive.selection
|
|
1813
|
+
: undefined;
|
|
1814
|
+
// A whole-message correction is an instruction, not representative task
|
|
1815
|
+
// language. Keep it as one explicit signal instead of double-counting the
|
|
1816
|
+
// language used to express the correction.
|
|
1817
|
+
const candidateValue = envelope.kind === 'text' && directive?.kind === 'ordinary-content'
|
|
1818
|
+
? observedResponseLanguage(envelope.text)
|
|
1819
|
+
: undefined;
|
|
1820
|
+
if (!this.isOwnerFeedbackController(binding, envelope))
|
|
1821
|
+
return undefined;
|
|
1822
|
+
const inbox = this.deliveryStore.getInboxByProviderEvent(envelope.channel, envelope.account, envelope.eventId);
|
|
1823
|
+
const durableReply = this.deliveryStore.getOutbox(reply.id);
|
|
1824
|
+
const principalLineage = this.ownerLineageForBinding(binding);
|
|
1825
|
+
if (inbox?.status !== 'claimed' || inbox.bindingId !== binding.id
|
|
1826
|
+
|| principalLineage === undefined
|
|
1827
|
+
|| durableReply?.id !== reply.id
|
|
1828
|
+
|| durableReply.intent.bindingId !== binding.id
|
|
1829
|
+
|| durableReply.intent.replyToEventId !== envelope.eventId
|
|
1830
|
+
|| durableReply.intent.idempotencyKey !== this.inboundReplyIdempotencyKey(binding, envelope.eventId)
|
|
1831
|
+
|| JSON.stringify(durableReply.intent.target.conversation) !== JSON.stringify(binding.conversation)
|
|
1832
|
+
|| JSON.stringify(durableReply.intent.target.principal) !== JSON.stringify(binding.principal)) {
|
|
1833
|
+
throw new AssistantDeliveryError('runtime-conflict', 'completed preference turn does not match its durable reply');
|
|
1834
|
+
}
|
|
1835
|
+
const selections = [];
|
|
1836
|
+
if (naturalSelection !== undefined)
|
|
1837
|
+
selections.push(naturalSelection);
|
|
1838
|
+
if (candidateValue !== undefined)
|
|
1839
|
+
selections.push({
|
|
1840
|
+
preferenceKey: 'response.language',
|
|
1841
|
+
candidateValue,
|
|
1842
|
+
interpretationTrust: 'typed-feedback',
|
|
1843
|
+
});
|
|
1844
|
+
const authorization = selections.length === 0
|
|
1845
|
+
? undefined
|
|
1846
|
+
: this.authorizeOwnerPreferenceFeedback(binding, envelope, selections);
|
|
1847
|
+
const completion = Object.freeze({
|
|
1848
|
+
bindingId: binding.id,
|
|
1849
|
+
bindingVersion: binding.version,
|
|
1850
|
+
sessionId: binding.sessionId,
|
|
1851
|
+
sourceEventId: envelope.eventId,
|
|
1852
|
+
sourceInboxId: inbox.id,
|
|
1853
|
+
replyOutboxId: durableReply.id,
|
|
1854
|
+
});
|
|
1855
|
+
const completionDigest = createHash('sha256')
|
|
1856
|
+
.update('assistant-delivery-completed-preference-binding-v2\0')
|
|
1857
|
+
.update(JSON.stringify([
|
|
1858
|
+
binding.id,
|
|
1859
|
+
binding.version,
|
|
1860
|
+
binding.sessionId,
|
|
1861
|
+
principalLineage.principalRecordId,
|
|
1862
|
+
principalLineage.principalVersion,
|
|
1863
|
+
inbox.admissionCursor.epoch,
|
|
1864
|
+
inbox.admissionCursor.sequence,
|
|
1865
|
+
inbox.id,
|
|
1866
|
+
durableReply.id,
|
|
1867
|
+
envelope.eventId,
|
|
1868
|
+
]))
|
|
1869
|
+
.digest('hex');
|
|
1870
|
+
const completionEvent = Object.freeze({
|
|
1871
|
+
scope: Object.freeze({ workspace: binding.workspace, preset: binding.agentPreset }),
|
|
1872
|
+
principalId: externalPrincipalId(binding.principal),
|
|
1873
|
+
principalLineage,
|
|
1874
|
+
admissionCursor: inbox.admissionCursor,
|
|
1875
|
+
actorTrust: 'owner-authenticated',
|
|
1876
|
+
source: 'delivery-completion',
|
|
1877
|
+
occurredAt: inbox.receivedAt,
|
|
1878
|
+
idempotencyKey: `delivery-completed-binding-v2:${completionDigest}`,
|
|
1879
|
+
completion,
|
|
1880
|
+
});
|
|
1881
|
+
const events = [];
|
|
1882
|
+
if (naturalSelection !== undefined && authorization !== undefined) {
|
|
1883
|
+
events.push(completionEvent);
|
|
1884
|
+
events.push(feedbackSignalInput(binding, envelope, naturalSelection, authorization));
|
|
1885
|
+
}
|
|
1886
|
+
if (candidateValue !== undefined && authorization !== undefined) {
|
|
1887
|
+
const digest = createHash('sha256')
|
|
1888
|
+
.update('assistant-delivery-completed-preference-turn-v2\0')
|
|
1889
|
+
.update(JSON.stringify([
|
|
1890
|
+
binding.id,
|
|
1891
|
+
binding.version,
|
|
1892
|
+
binding.sessionId,
|
|
1893
|
+
principalLineage.principalRecordId,
|
|
1894
|
+
principalLineage.principalVersion,
|
|
1895
|
+
inbox.admissionCursor.epoch,
|
|
1896
|
+
inbox.admissionCursor.sequence,
|
|
1897
|
+
inbox.id,
|
|
1898
|
+
durableReply.id,
|
|
1899
|
+
envelope.eventId,
|
|
1900
|
+
candidateValue,
|
|
1901
|
+
]))
|
|
1902
|
+
.digest('hex');
|
|
1903
|
+
const event = Object.freeze({
|
|
1904
|
+
scope: Object.freeze({ workspace: binding.workspace, preset: binding.agentPreset }),
|
|
1905
|
+
principalId: externalPrincipalId(binding.principal),
|
|
1906
|
+
principalLineage,
|
|
1907
|
+
admissionCursor: inbox.admissionCursor,
|
|
1908
|
+
preferenceKey: 'response.language',
|
|
1909
|
+
candidateValue,
|
|
1910
|
+
stance: 'support',
|
|
1911
|
+
actorTrust: 'owner-authenticated',
|
|
1912
|
+
interpretationTrust: 'behavioral-inference',
|
|
1913
|
+
source: 'delivery-observation',
|
|
1914
|
+
occurredAt: inbox.receivedAt,
|
|
1915
|
+
idempotencyKey: `delivery-completed-turn-v2:${digest}`,
|
|
1916
|
+
completion,
|
|
1917
|
+
});
|
|
1918
|
+
events.push(event);
|
|
1919
|
+
}
|
|
1920
|
+
if (events.length === 0)
|
|
1921
|
+
events.push(completionEvent);
|
|
1922
|
+
return Object.freeze(events);
|
|
1923
|
+
}
|
|
1924
|
+
async dispatchWorkflowCommand(binding, envelope, command) {
|
|
1925
|
+
const replyTarget = envelope.metadata?.replyToProviderMessageId;
|
|
1926
|
+
if (typeof replyTarget !== 'string' || !this.isOwnerFeedbackController(binding, envelope)) {
|
|
1927
|
+
return 'invalid-target';
|
|
1928
|
+
}
|
|
1929
|
+
const reviewInbox = this.deliveryStore.getInboxByProviderEvent(envelope.channel, envelope.account, envelope.eventId);
|
|
1930
|
+
const sourceOutbox = this.deliveryStore.getOutboxByProviderMessage(envelope.channel, envelope.account, replyTarget);
|
|
1931
|
+
const sourceEventId = sourceOutbox?.intent.replyToEventId;
|
|
1932
|
+
const sourceInbox = sourceEventId === undefined
|
|
1933
|
+
? undefined
|
|
1934
|
+
: this.deliveryStore.getInboxByProviderEvent(envelope.channel, envelope.account, sourceEventId);
|
|
1935
|
+
if (reviewInbox?.status !== 'claimed' || reviewInbox.bindingId !== binding.id
|
|
1936
|
+
|| sourceInbox?.status !== 'processed' || sourceInbox.bindingId !== binding.id
|
|
1937
|
+
|| sourceInbox.envelope.kind !== 'text'
|
|
1938
|
+
|| (sourceInbox.envelope.attachments?.length ?? 0) !== 0
|
|
1939
|
+
|| sourceOutbox === undefined || sourceOutbox.providerMessageId !== replyTarget
|
|
1940
|
+
|| sourceOutbox.intent.bindingId !== binding.id
|
|
1941
|
+
|| JSON.stringify(sourceInbox.envelope.conversation) !== JSON.stringify(binding.conversation)
|
|
1942
|
+
|| JSON.stringify(sourceInbox.envelope.principal) !== JSON.stringify(binding.principal)
|
|
1943
|
+
|| JSON.stringify(sourceOutbox.intent.target.conversation) !== JSON.stringify(binding.conversation)
|
|
1944
|
+
|| JSON.stringify(sourceOutbox.intent.target.principal) !== JSON.stringify(binding.principal)) {
|
|
1945
|
+
return 'invalid-target';
|
|
1946
|
+
}
|
|
1947
|
+
const subjectRef = growthObjectDigest({
|
|
1948
|
+
contract: 'assistant-delivery-owner-workflow-subject/v1',
|
|
1949
|
+
workspace: binding.workspace,
|
|
1950
|
+
preset: binding.agentPreset,
|
|
1951
|
+
bindingId: binding.id,
|
|
1952
|
+
sourceInboxId: sourceInbox.id,
|
|
1953
|
+
sourceOutboxId: sourceOutbox.id,
|
|
1954
|
+
});
|
|
1955
|
+
const taskRef = growthObjectDigest({
|
|
1956
|
+
contract: 'assistant-delivery-owner-task-ref/v1',
|
|
1957
|
+
workspace: binding.workspace,
|
|
1958
|
+
preset: binding.agentPreset,
|
|
1959
|
+
sourceInboxId: sourceInbox.id,
|
|
1960
|
+
});
|
|
1961
|
+
const payloadDigest = growthObjectDigest({
|
|
1962
|
+
contract: 'assistant-delivery-owner-workflow-command/v1',
|
|
1963
|
+
action: command.kind === 'save' ? 'upsert' : 'retract',
|
|
1964
|
+
reviewInboxId: reviewInbox.id,
|
|
1965
|
+
sourceInboxId: sourceInbox.id,
|
|
1966
|
+
sourceOutboxId: sourceOutbox.id,
|
|
1967
|
+
subjectRef,
|
|
1968
|
+
...(command.kind === 'save'
|
|
1969
|
+
? { name: command.name, cron: command.cron, timezone: command.timezone }
|
|
1970
|
+
: {}),
|
|
1971
|
+
});
|
|
1972
|
+
try {
|
|
1973
|
+
this.deliveryStore.commitOwnerWorkflowTraceCommand({
|
|
1974
|
+
action: command.kind === 'save' ? 'upsert' : 'retract',
|
|
1975
|
+
operationId: `workflow-command:${reviewInbox.id}`,
|
|
1976
|
+
payloadDigest,
|
|
1977
|
+
binding,
|
|
1978
|
+
reviewInboxId: reviewInbox.id,
|
|
1979
|
+
sourceInboxId: sourceInbox.id,
|
|
1980
|
+
sourceOutboxId: sourceOutbox.id,
|
|
1981
|
+
subjectRef,
|
|
1982
|
+
occurredAt: sourceInbox.receivedAt,
|
|
1983
|
+
...(command.kind === 'save' ? {
|
|
1984
|
+
taskRef,
|
|
1985
|
+
templateContent: {
|
|
1986
|
+
scope: { workspace: binding.workspace, preset: binding.agentPreset },
|
|
1987
|
+
ownerBindingId: binding.id,
|
|
1988
|
+
name: command.name,
|
|
1989
|
+
prompt: sourceInbox.envelope.text,
|
|
1990
|
+
schedule: { kind: 'cron', expression: command.cron, timezone: command.timezone },
|
|
1991
|
+
timeoutMs: 60_000,
|
|
1992
|
+
toolCatalogIds: ['assistant.agent-turn'],
|
|
1993
|
+
deliveryBindingId: binding.id,
|
|
1994
|
+
},
|
|
1995
|
+
steps: [{
|
|
1996
|
+
catalogId: 'assistant.agent-turn',
|
|
1997
|
+
argumentSchemaDigest: workflowArgumentShapeDigest({ prompt: sourceInbox.envelope.text }),
|
|
1998
|
+
}],
|
|
1999
|
+
} : {}),
|
|
2000
|
+
});
|
|
2001
|
+
}
|
|
2002
|
+
catch (error) {
|
|
2003
|
+
if (error instanceof DeliveryStoreError) {
|
|
2004
|
+
if (error.code === 'idempotency-conflict' || error.code === 'version-conflict')
|
|
2005
|
+
return 'conflict';
|
|
2006
|
+
if (error.code === 'invalid-binding' || error.code === 'unauthorized-principal'
|
|
2007
|
+
|| error.code === 'receipt-mismatch' || error.code === 'conflict')
|
|
2008
|
+
return 'invalid-target';
|
|
2009
|
+
}
|
|
2010
|
+
return 'invalid-target';
|
|
2011
|
+
}
|
|
2012
|
+
void this.drainWorkflowTraces();
|
|
2013
|
+
return 'recorded';
|
|
2014
|
+
}
|
|
2015
|
+
/**
|
|
2016
|
+
* Convert only an authenticated reply to one exact Automation delivery into
|
|
2017
|
+
* a trusted objective label. The original run timestamp is retained because
|
|
2018
|
+
* Evolution re-proves the immutable production run; the later Inbox is a
|
|
2019
|
+
* separate evidence reference, never copied as prose.
|
|
2020
|
+
*/
|
|
2021
|
+
async dispatchObjectiveFeedback(binding, envelope, objectiveStatus) {
|
|
2022
|
+
const replyTarget = envelope.metadata?.replyToProviderMessageId;
|
|
2023
|
+
if (typeof replyTarget !== 'string')
|
|
2024
|
+
return 'invalid-target';
|
|
2025
|
+
if (!this.isOwnerFeedbackController(binding, envelope))
|
|
2026
|
+
return 'invalid-target';
|
|
2027
|
+
const inbox = this.deliveryStore.getInboxByProviderEvent(envelope.channel, envelope.account, envelope.eventId);
|
|
2028
|
+
const target = this.deliveryStore.getOutboxByProviderMessage(envelope.channel, envelope.account, replyTarget);
|
|
2029
|
+
if (inbox?.status !== 'claimed' || inbox.bindingId !== binding.id
|
|
2030
|
+
|| target === undefined || target.providerMessageId !== replyTarget
|
|
2031
|
+
|| target.intent.bindingId !== binding.id
|
|
2032
|
+
|| JSON.stringify(target.intent.target.conversation) !== JSON.stringify(binding.conversation)
|
|
2033
|
+
|| JSON.stringify(target.intent.target.principal) !== JSON.stringify(binding.principal)) {
|
|
2034
|
+
return 'invalid-target';
|
|
2035
|
+
}
|
|
2036
|
+
const metadata = target.intent.metadata;
|
|
2037
|
+
// Ordinary Agent replies have no learning metadata. They are eligible
|
|
2038
|
+
// only for Delivery's local, atomic verified-workflow receipt; the Store
|
|
2039
|
+
// rebuilds the full Inbox/Outbox fence and abstains unless the source text
|
|
2040
|
+
// selects a closed deterministic-deidentification template. Do not route
|
|
2041
|
+
// these judgements through Evaluation: they are task proof for workflow
|
|
2042
|
+
// repetition, not a cross-plugin quality vote.
|
|
2043
|
+
if (metadata?.['dsh.learning.kind'] !== 'automation-run') {
|
|
2044
|
+
const sourceEventId = target.intent.replyToEventId;
|
|
2045
|
+
if (typeof sourceEventId !== 'string')
|
|
2046
|
+
return 'invalid-target';
|
|
2047
|
+
const sourceInbox = this.deliveryStore.getInboxByProviderEvent(envelope.channel, envelope.account, sourceEventId);
|
|
2048
|
+
if (sourceInbox === undefined)
|
|
2049
|
+
return 'invalid-target';
|
|
2050
|
+
try {
|
|
2051
|
+
const recorded = this.deliveryStore.commitVerifiedWorkflowTraceFeedback({
|
|
2052
|
+
binding,
|
|
2053
|
+
feedbackInboxId: inbox.id,
|
|
2054
|
+
sourceInboxId: sourceInbox.id,
|
|
2055
|
+
sourceOutboxId: target.id,
|
|
2056
|
+
objectiveStatus,
|
|
2057
|
+
});
|
|
2058
|
+
if (recorded.outcome === 'trace-recorded')
|
|
2059
|
+
void this.drainWorkflowTraces();
|
|
2060
|
+
return 'recorded';
|
|
2061
|
+
}
|
|
2062
|
+
catch (error) {
|
|
2063
|
+
if (error instanceof DeliveryStoreError) {
|
|
2064
|
+
if (error.code === 'idempotency-conflict' || error.code === 'version-conflict') {
|
|
2065
|
+
return 'conflict';
|
|
2066
|
+
}
|
|
2067
|
+
if (error.code === 'invalid-binding' || error.code === 'unauthorized-principal'
|
|
2068
|
+
|| error.code === 'receipt-mismatch' || error.code === 'conflict'
|
|
2069
|
+
|| error.code === 'not-found')
|
|
2070
|
+
return 'invalid-target';
|
|
2071
|
+
}
|
|
2072
|
+
return 'invalid-target';
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
const automationId = metadata?.['dsh.learning.automationId'];
|
|
2076
|
+
const runId = metadata?.['dsh.learning.runId'];
|
|
2077
|
+
const situation = metadata?.['dsh.learning.situation'];
|
|
2078
|
+
const occurredAtText = metadata?.['dsh.learning.occurredAt'];
|
|
2079
|
+
const outputDigest = metadata?.['dsh.learning.outputDigest'];
|
|
2080
|
+
const proofDigest = metadata?.['dsh.learning.proofDigest'];
|
|
2081
|
+
if (metadata?.['dsh.learning.schemaVersion'] !== '2'
|
|
2082
|
+
|| metadata['dsh.learning.kind'] !== 'automation-run'
|
|
2083
|
+
|| metadata['dsh.learning.executionStatus'] !== 'succeeded'
|
|
2084
|
+
|| typeof automationId !== 'string' || typeof runId !== 'string'
|
|
2085
|
+
|| typeof situation !== 'string' || situation !== `automation:${automationId}`
|
|
2086
|
+
|| typeof outputDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(outputDigest)
|
|
2087
|
+
|| typeof proofDigest !== 'string' || !/^[a-f0-9]{64}$/u.test(proofDigest)
|
|
2088
|
+
|| typeof occurredAtText !== 'string' || !/^(0|[1-9][0-9]{0,15})$/u.test(occurredAtText)) {
|
|
2089
|
+
return 'invalid-target';
|
|
2090
|
+
}
|
|
2091
|
+
const occurredAt = Number(occurredAtText);
|
|
2092
|
+
if (!Number.isSafeInteger(occurredAt) || occurredAt > inbox.receivedAt)
|
|
2093
|
+
return 'invalid-target';
|
|
2094
|
+
if (!target.intent.text.endsWith(automationObjectiveFeedbackFooter))
|
|
2095
|
+
return 'invalid-target';
|
|
2096
|
+
const outputPreview = target.intent.text.slice(0, -automationObjectiveFeedbackFooter.length);
|
|
2097
|
+
if (createHash('sha256').update(outputPreview).digest('hex') !== outputDigest)
|
|
2098
|
+
return 'invalid-target';
|
|
2099
|
+
const resolver = this.context.get('assistantAutomations');
|
|
2100
|
+
if (!isAutomationDeliveryEvidenceResolver(resolver))
|
|
2101
|
+
return 'unavailable';
|
|
2102
|
+
let proof;
|
|
2103
|
+
try {
|
|
2104
|
+
proof = resolver.resolveDeliveryEvidence({
|
|
2105
|
+
automationId,
|
|
2106
|
+
runId,
|
|
2107
|
+
expectedWorkspace: binding.workspace,
|
|
2108
|
+
expectedBindingId: binding.id,
|
|
2109
|
+
expectedOutputDigest: outputDigest,
|
|
2110
|
+
});
|
|
2111
|
+
}
|
|
2112
|
+
catch {
|
|
2113
|
+
return 'invalid-target';
|
|
2114
|
+
}
|
|
2115
|
+
if (proof === undefined || proof.schemaVersion !== 1 || proof.source !== 'assistant-automations'
|
|
2116
|
+
|| proof.executionKind !== 'agent' || proof.executionStatus !== 'succeeded'
|
|
2117
|
+
|| proof.automationId !== automationId || proof.runId !== runId
|
|
2118
|
+
|| proof.workspace !== binding.workspace || proof.agentPreset !== binding.agentPreset
|
|
2119
|
+
|| proof.bindingId !== binding.id || proof.situation !== situation
|
|
2120
|
+
|| proof.occurredAt !== occurredAt || proof.outputDigest !== outputDigest
|
|
2121
|
+
|| proof.proofDigest !== proofDigest)
|
|
2122
|
+
return 'invalid-target';
|
|
2123
|
+
const sink = this.evaluationSink;
|
|
2124
|
+
if (sink === undefined)
|
|
2125
|
+
return 'unavailable';
|
|
2126
|
+
// One immutable Automation run is one learning subject. The objective
|
|
2127
|
+
// status and Inbox event deliberately do not enter the key: replays and
|
|
2128
|
+
// repeated equal judgements collapse, while an opposite judgement becomes
|
|
2129
|
+
// an explicit conflict instead of a second Evolution vote.
|
|
2130
|
+
const digest = createHash('sha256').update('assistant-delivery-objective-feedback-v2\0')
|
|
2131
|
+
.update(JSON.stringify([binding.workspace, binding.agentPreset, target.id, runId]))
|
|
2132
|
+
.digest('hex');
|
|
2133
|
+
const claims = Object.freeze({
|
|
2134
|
+
scope: Object.freeze({ workspace: binding.workspace, preset: binding.agentPreset }),
|
|
2135
|
+
situation,
|
|
2136
|
+
objectiveStatus,
|
|
2137
|
+
runId,
|
|
2138
|
+
outboxId: target.id,
|
|
2139
|
+
chatId: binding.conversation.chat,
|
|
2140
|
+
principalId: externalPrincipalId(binding.principal),
|
|
2141
|
+
bindingId: binding.id,
|
|
2142
|
+
occurredAt,
|
|
2143
|
+
idempotencyKey: `assistant-delivery:objective-feedback-v2:${digest}`,
|
|
2144
|
+
});
|
|
2145
|
+
try {
|
|
2146
|
+
const capabilityReceipt = sink.registration.issueCapability(claims);
|
|
2147
|
+
if (this.evaluationSink?.token !== sink.token)
|
|
2148
|
+
return 'unknown';
|
|
2149
|
+
const receipt = await Promise.resolve(sink.registration.append({
|
|
2150
|
+
capabilityReceipt,
|
|
2151
|
+
runId,
|
|
2152
|
+
outboxId: target.id,
|
|
2153
|
+
chatId: binding.conversation.chat,
|
|
2154
|
+
principalId: externalPrincipalId(binding.principal),
|
|
2155
|
+
bindingId: binding.id,
|
|
2156
|
+
idempotencyKey: claims.idempotencyKey,
|
|
2157
|
+
}));
|
|
2158
|
+
if (typeof receipt !== 'object' || receipt === null
|
|
2159
|
+
|| receipt.idempotencyKey !== claims.idempotencyKey) {
|
|
2160
|
+
return 'unknown';
|
|
2161
|
+
}
|
|
2162
|
+
return 'recorded';
|
|
2163
|
+
}
|
|
2164
|
+
catch (error) {
|
|
2165
|
+
if (typeof error === 'object' && error !== null && 'code' in error
|
|
2166
|
+
&& error.code === 'idempotency-conflict')
|
|
2167
|
+
return 'conflict';
|
|
2168
|
+
return 'unknown';
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
827
2171
|
authorizePermissionReply(binding, envelope) {
|
|
828
2172
|
if (!this.isPermissionController(binding, envelope))
|
|
829
2173
|
return false;
|
|
@@ -1228,6 +2572,9 @@ export class AssistantDeliveryService extends Service {
|
|
|
1228
2572
|
}
|
|
1229
2573
|
enqueueBackground(input) {
|
|
1230
2574
|
this.assertActive();
|
|
2575
|
+
if (containsReservedLearningMetadata(input.metadata)) {
|
|
2576
|
+
throw new AssistantDeliveryError('runtime-conflict', 'reserved learning metadata requires the typed Automation result seam');
|
|
2577
|
+
}
|
|
1231
2578
|
const binding = this.deliveryStore.getBinding(input.bindingId);
|
|
1232
2579
|
if (binding === undefined || binding.status !== 'active' || !this.hasActivePrincipal(binding)) {
|
|
1233
2580
|
throw new AssistantDeliveryError('missing-binding', 'delivery binding does not exist or is revoked');
|
|
@@ -1245,15 +2592,198 @@ export class AssistantDeliveryService extends Service {
|
|
|
1245
2592
|
}
|
|
1246
2593
|
return this.deliveryStore.enqueue({ idempotencyKey: input.idempotencyKey, bindingId: binding.id,
|
|
1247
2594
|
target: { conversation: binding.conversation, principal: binding.principal }, text: input.text,
|
|
1248
|
-
format: input.format ?? 'plain'
|
|
2595
|
+
format: input.format ?? 'plain',
|
|
2596
|
+
...(input.metadata === undefined ? {} : { metadata: input.metadata }) });
|
|
1249
2597
|
}
|
|
1250
|
-
|
|
2598
|
+
/**
|
|
2599
|
+
* Dedicated Automation result lane. Every learning field is derived from an
|
|
2600
|
+
* authoritative Automations receipt; callers cannot supply metadata,
|
|
2601
|
+
* idempotency, source identity, occurrence, timestamp, or feedback text.
|
|
2602
|
+
*/
|
|
2603
|
+
enqueueAutomationResult(input) {
|
|
1251
2604
|
this.assertActive();
|
|
2605
|
+
if (typeof input.outputPreview !== 'string') {
|
|
2606
|
+
throw new AssistantDeliveryError('runtime-conflict', 'Automation result output is invalid');
|
|
2607
|
+
}
|
|
2608
|
+
const outputDigest = createHash('sha256').update(input.outputPreview).digest('hex');
|
|
2609
|
+
const resolver = this.context.get('assistantAutomations');
|
|
2610
|
+
if (!isAutomationDeliveryEvidenceResolver(resolver)) {
|
|
2611
|
+
throw new AssistantDeliveryError('runtime-conflict', 'Automation delivery evidence is unavailable');
|
|
2612
|
+
}
|
|
2613
|
+
let proof;
|
|
2614
|
+
try {
|
|
2615
|
+
proof = resolver.resolveDeliveryEvidence({
|
|
2616
|
+
automationId: input.automationId,
|
|
2617
|
+
runId: input.runId,
|
|
2618
|
+
expectedWorkspace: input.workspace,
|
|
2619
|
+
expectedBindingId: input.bindingId,
|
|
2620
|
+
expectedOutputDigest: outputDigest,
|
|
2621
|
+
});
|
|
2622
|
+
}
|
|
2623
|
+
catch {
|
|
2624
|
+
throw new AssistantDeliveryError('runtime-conflict', 'Automation delivery evidence was rejected');
|
|
2625
|
+
}
|
|
2626
|
+
if (proof === undefined || proof.schemaVersion !== 1 || proof.source !== 'assistant-automations'
|
|
2627
|
+
|| proof.executionKind !== 'agent' || proof.executionStatus !== 'succeeded'
|
|
2628
|
+
|| proof.automationId !== input.automationId || proof.runId !== input.runId
|
|
2629
|
+
|| proof.workspace !== input.workspace || proof.bindingId !== input.bindingId
|
|
2630
|
+
|| proof.situation !== `automation:${input.automationId}`
|
|
2631
|
+
|| proof.outputDigest !== outputDigest || !/^[a-f0-9]{64}$/u.test(proof.proofDigest)
|
|
2632
|
+
|| !Number.isSafeInteger(proof.occurredAt) || proof.occurredAt < 0) {
|
|
2633
|
+
throw new AssistantDeliveryError('runtime-conflict', 'Automation delivery evidence is mismatched');
|
|
2634
|
+
}
|
|
1252
2635
|
const binding = this.deliveryStore.getBinding(input.bindingId);
|
|
2636
|
+
if (binding === undefined || binding.status !== 'active' || !this.hasActivePrincipal(binding)
|
|
2637
|
+
|| binding.workspace !== input.workspace || binding.agentPreset !== proof.agentPreset) {
|
|
2638
|
+
throw new AssistantDeliveryError('missing-binding', 'Automation result binding is unavailable or mismatched');
|
|
2639
|
+
}
|
|
2640
|
+
const decision = this.policy.authorize({
|
|
2641
|
+
subject: { kind: 'background', id: proof.automationId,
|
|
2642
|
+
workspace: proof.workspace, principal: externalPrincipalId(binding.principal) },
|
|
2643
|
+
action: 'send', resource: { kind: 'message', id: binding.id }, context: { initiator: 'background' },
|
|
2644
|
+
}, { idempotencyKey: `message-send:automation:${proof.occurrenceId}:${binding.id}` });
|
|
2645
|
+
if (decision.effect !== 'allow')
|
|
2646
|
+
throw policyDenied(decision);
|
|
2647
|
+
if (!this.hasActivePrincipal(binding)) {
|
|
2648
|
+
throw new AssistantDeliveryError('missing-binding', 'delivery binding principal is no longer active');
|
|
2649
|
+
}
|
|
2650
|
+
const text = `${input.outputPreview}${automationObjectiveFeedbackFooter}`;
|
|
2651
|
+
if (Buffer.byteLength(text, 'utf8') > this.config.maxTextBytes) {
|
|
2652
|
+
throw new AssistantDeliveryError('runtime-conflict', 'Automation result exceeds the delivery text limit');
|
|
2653
|
+
}
|
|
2654
|
+
return this.deliveryStore.enqueue({
|
|
2655
|
+
idempotencyKey: `automation:${proof.occurrenceId}:${binding.id}`,
|
|
2656
|
+
bindingId: binding.id,
|
|
2657
|
+
target: { conversation: binding.conversation, principal: binding.principal },
|
|
2658
|
+
text,
|
|
2659
|
+
format: 'markdown',
|
|
2660
|
+
metadata: Object.freeze({
|
|
2661
|
+
'dsh.learning.schemaVersion': '2',
|
|
2662
|
+
'dsh.learning.kind': 'automation-run',
|
|
2663
|
+
'dsh.learning.automationId': proof.automationId,
|
|
2664
|
+
'dsh.learning.runId': proof.runId,
|
|
2665
|
+
'dsh.learning.situation': proof.situation,
|
|
2666
|
+
'dsh.learning.occurredAt': String(proof.occurredAt),
|
|
2667
|
+
'dsh.learning.executionStatus': proof.executionStatus,
|
|
2668
|
+
'dsh.learning.outputDigest': proof.outputDigest,
|
|
2669
|
+
'dsh.learning.proofDigest': proof.proofDigest,
|
|
2670
|
+
}),
|
|
2671
|
+
});
|
|
2672
|
+
}
|
|
2673
|
+
/** Resolve the current generation of one exact Host-owned owner route. */
|
|
2674
|
+
resolveOwnerRoute(authorityIdInput) {
|
|
2675
|
+
this.assertActive();
|
|
2676
|
+
const authorityId = canonicalOwnerRouteId(authorityIdInput);
|
|
2677
|
+
const authority = this.ownerRoutes.get(authorityId);
|
|
2678
|
+
if (authority === undefined) {
|
|
2679
|
+
throw new AssistantDeliveryError('missing-binding', 'owner route authority is not configured');
|
|
2680
|
+
}
|
|
2681
|
+
const binding = this.deliveryStore.getActiveBinding(authority.conversation);
|
|
2682
|
+
const owner = binding === undefined ? undefined : this.deliveryStore.getPrincipal(binding.principal);
|
|
2683
|
+
if (binding === undefined || binding.status !== 'active'
|
|
2684
|
+
|| !bindingMatchesOwnerRoute(binding, authority)
|
|
2685
|
+
|| owner?.status !== 'active' || owner.role !== 'owner') {
|
|
2686
|
+
throw new AssistantDeliveryError('missing-binding', 'active binding does not match owner route authority');
|
|
2687
|
+
}
|
|
2688
|
+
return {
|
|
2689
|
+
authorityId,
|
|
2690
|
+
binding,
|
|
2691
|
+
snapshot: ownerRouteBindingSnapshot(authority, binding),
|
|
2692
|
+
};
|
|
2693
|
+
}
|
|
2694
|
+
/**
|
|
2695
|
+
* Revalidate Recovery/other Host work against the live owner route without
|
|
2696
|
+
* exposing a session id, conversation id, user message or delivery content.
|
|
2697
|
+
*/
|
|
2698
|
+
validateOwnerRoute(input) {
|
|
2699
|
+
this.assertActive();
|
|
2700
|
+
const resolved = this.resolveOwnerRoute(input.authorityId);
|
|
2701
|
+
const authority = this.ownerRoutes.get(resolved.authorityId);
|
|
2702
|
+
const principalLineage = this.ownerLineageForBinding(resolved.binding);
|
|
2703
|
+
if (principalLineage === undefined) {
|
|
2704
|
+
throw new AssistantDeliveryError('missing-binding', 'owner route principal lineage is unavailable');
|
|
2705
|
+
}
|
|
2706
|
+
const expectedPrincipal = externalPrincipalId(authority.principal);
|
|
2707
|
+
const text = (value, label, maxBytes) => {
|
|
2708
|
+
if (typeof value !== 'string') {
|
|
2709
|
+
throw new AssistantDeliveryError('missing-binding', `owner route ${label} is invalid`);
|
|
2710
|
+
}
|
|
2711
|
+
const normalized = value.normalize('NFC').trim();
|
|
2712
|
+
const hasControl = [...normalized].some(character => {
|
|
2713
|
+
const point = character.codePointAt(0);
|
|
2714
|
+
return point <= 0x1f || point === 0x7f;
|
|
2715
|
+
});
|
|
2716
|
+
if (normalized === '' || Buffer.byteLength(normalized, 'utf8') > maxBytes || hasControl) {
|
|
2717
|
+
throw new AssistantDeliveryError('missing-binding', `owner route ${label} is invalid`);
|
|
2718
|
+
}
|
|
2719
|
+
return normalized;
|
|
2720
|
+
};
|
|
2721
|
+
const principalId = text(input.principalId, 'principal', 1_024);
|
|
2722
|
+
const workspace = text(input.workspace, 'workspace', 4_096);
|
|
2723
|
+
const agentPreset = text(input.agentPreset, 'preset', 200);
|
|
2724
|
+
if (principalId !== expectedPrincipal || workspace !== authority.workspace
|
|
2725
|
+
|| agentPreset !== authority.agentPreset) {
|
|
2726
|
+
throw new AssistantDeliveryError('missing-binding', 'owner route authority does not match the requested scope');
|
|
2727
|
+
}
|
|
2728
|
+
return Object.freeze({
|
|
2729
|
+
receiptVersion: 2,
|
|
2730
|
+
authorityId: resolved.authorityId,
|
|
2731
|
+
authorityHash: resolved.snapshot.authorityHash,
|
|
2732
|
+
principalId,
|
|
2733
|
+
principalRecordId: principalLineage.principalRecordId,
|
|
2734
|
+
principalVersion: principalLineage.principalVersion,
|
|
2735
|
+
workspace,
|
|
2736
|
+
agentPreset,
|
|
2737
|
+
bindingVersion: resolved.snapshot.bindingVersion,
|
|
2738
|
+
generation: resolved.snapshot.generation,
|
|
2739
|
+
});
|
|
2740
|
+
}
|
|
2741
|
+
/**
|
|
2742
|
+
* Enqueue against a stable Host-owned route rather than a session binding.
|
|
2743
|
+
* Policy is evaluated against the exact `route:<authorityId>` resource. The
|
|
2744
|
+
* Store re-resolves under its write transaction, so a concurrent `/new`
|
|
2745
|
+
* either wins completely or follows the earlier immutable Outbox receipt.
|
|
2746
|
+
*/
|
|
2747
|
+
enqueueBackgroundRoute(input) {
|
|
2748
|
+
this.assertActive();
|
|
2749
|
+
const sourceId = canonicalBackgroundSourceId(input.sourceId);
|
|
2750
|
+
const idempotencyKey = canonicalRouteIdempotencyKey(input.idempotencyKey);
|
|
2751
|
+
if (typeof input.text !== 'string' || Buffer.byteLength(input.text, 'utf8') > this.config.maxTextBytes
|
|
2752
|
+
|| (input.format !== undefined && input.format !== 'markdown' && input.format !== 'plain')) {
|
|
2753
|
+
throw new AssistantDeliveryError('runtime-conflict', 'owner route message is invalid');
|
|
2754
|
+
}
|
|
2755
|
+
const resolved = this.resolveOwnerRoute(input.authorityId);
|
|
2756
|
+
const authority = this.ownerRoutes.get(resolved.authorityId);
|
|
2757
|
+
const decision = authorizeOwnerRoute(this.policy, authority, sourceId, idempotencyKey);
|
|
2758
|
+
if (decision.effect !== 'allow')
|
|
2759
|
+
throw policyDenied(decision);
|
|
2760
|
+
try {
|
|
2761
|
+
return this.deliveryStore.enqueueOwnerRoute({
|
|
2762
|
+
authority,
|
|
2763
|
+
sourceId,
|
|
2764
|
+
sourceHash: createHash('sha256').update(sourceId).digest('hex'),
|
|
2765
|
+
idempotencyKey,
|
|
2766
|
+
text: input.text,
|
|
2767
|
+
format: input.format ?? 'plain',
|
|
2768
|
+
});
|
|
2769
|
+
}
|
|
2770
|
+
catch (error) {
|
|
2771
|
+
if (error instanceof DeliveryStoreError && error.code === 'invalid-binding') {
|
|
2772
|
+
throw new AssistantDeliveryError('missing-binding', 'owner route changed during enqueue');
|
|
2773
|
+
}
|
|
2774
|
+
throw error;
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
enqueueApproval(input) {
|
|
2778
|
+
this.assertActive();
|
|
2779
|
+
if (input.route?.routeVersion !== 2) {
|
|
2780
|
+
throw new AssistantDeliveryError('missing-binding', 'approval dispatch requires an exact v2 owner route');
|
|
2781
|
+
}
|
|
2782
|
+
const binding = this.deliveryStore.getBinding(input.route.bindingId);
|
|
1253
2783
|
if (binding === undefined || binding.status !== 'active') {
|
|
1254
2784
|
throw new AssistantDeliveryError('missing-binding', 'delivery binding does not exist or is revoked');
|
|
1255
2785
|
}
|
|
1256
|
-
if (binding.workspace !== input.workspace) {
|
|
2786
|
+
if (binding.workspace !== input.route.workspace) {
|
|
1257
2787
|
throw new AssistantDeliveryError('missing-binding', 'delivery workspace does not match the active binding');
|
|
1258
2788
|
}
|
|
1259
2789
|
const owner = this.deliveryStore.getPrincipal(binding.principal);
|
|
@@ -1269,14 +2799,23 @@ export class AssistantDeliveryService extends Service {
|
|
|
1269
2799
|
|| createHash('sha256').update(input.text).digest('hex') !== proposal.diffHash) {
|
|
1270
2800
|
throw new AssistantDeliveryError('missing-binding', 'approval card does not match its active owner and Policy proposal');
|
|
1271
2801
|
}
|
|
1272
|
-
const decision = this.policy.authorize({ subject: { kind: 'background', id: input.sourceId,
|
|
1273
|
-
workspace: input.workspace, principal }, action: 'approval.send',
|
|
2802
|
+
const decision = this.policy.authorize({ subject: { kind: 'background', id: input.route.sourceId,
|
|
2803
|
+
workspace: input.route.workspace, principal }, action: 'approval.send',
|
|
1274
2804
|
resource: { kind: 'message', id: binding.id }, context: { initiator: 'background' } }, { idempotencyKey: `message-approval:${input.idempotencyKey}` });
|
|
1275
2805
|
if (decision.effect !== 'allow')
|
|
1276
2806
|
throw policyDenied(decision);
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
2807
|
+
try {
|
|
2808
|
+
return this.deliveryStore.enqueueApprovalRoute({ route: input.route, intent: {
|
|
2809
|
+
idempotencyKey: input.idempotencyKey, text: input.text, approval: input.approval,
|
|
2810
|
+
} });
|
|
2811
|
+
}
|
|
2812
|
+
catch (error) {
|
|
2813
|
+
if (error instanceof DeliveryStoreError
|
|
2814
|
+
&& (error.code === 'invalid-binding' || error.code === 'invalid-intent')) {
|
|
2815
|
+
throw new AssistantDeliveryError('missing-binding', 'approval route changed or is invalid');
|
|
2816
|
+
}
|
|
2817
|
+
throw error;
|
|
2818
|
+
}
|
|
1280
2819
|
}
|
|
1281
2820
|
replyCommand(bindingInput, input) {
|
|
1282
2821
|
this.assertActive();
|
|
@@ -1335,6 +2874,33 @@ export class AssistantDeliveryService extends Service {
|
|
|
1335
2874
|
return adapter?.capabilities.formats.includes('markdown') === true ? 'markdown' : 'plain';
|
|
1336
2875
|
}
|
|
1337
2876
|
reply(agent, input) {
|
|
2877
|
+
const prepared = this.prepareAgentReply(agent, input);
|
|
2878
|
+
return this.deliveryStore.enqueue(prepared.intent);
|
|
2879
|
+
}
|
|
2880
|
+
async replyCompletedPreferenceTurn(agent, expectedBinding, envelope, input) {
|
|
2881
|
+
const prepared = this.prepareAgentReply(agent, {
|
|
2882
|
+
idempotencyKey: `inbound:${envelope.eventId}:reply`,
|
|
2883
|
+
replyToEventId: envelope.eventId,
|
|
2884
|
+
...input,
|
|
2885
|
+
});
|
|
2886
|
+
if (prepared.binding.id !== expectedBinding.id
|
|
2887
|
+
|| prepared.binding.version !== expectedBinding.version
|
|
2888
|
+
|| prepared.binding.sessionId !== expectedBinding.sessionId
|
|
2889
|
+
|| JSON.stringify(envelope.conversation) !== JSON.stringify(prepared.binding.conversation)
|
|
2890
|
+
|| JSON.stringify(envelope.principal) !== JSON.stringify(prepared.binding.principal)) {
|
|
2891
|
+
throw new AssistantDeliveryError('missing-binding', 'completed turn no longer belongs to its exact Delivery binding');
|
|
2892
|
+
}
|
|
2893
|
+
let projected = false;
|
|
2894
|
+
this.deliveryStore.enqueueReplyWithPreferenceProjection(prepared.intent, reply => {
|
|
2895
|
+
const events = this.completedPreferenceEvents(prepared.binding, envelope, reply);
|
|
2896
|
+
projected = events !== undefined;
|
|
2897
|
+
return events;
|
|
2898
|
+
});
|
|
2899
|
+
if (projected)
|
|
2900
|
+
await this.drainPreferenceProjections();
|
|
2901
|
+
return projected ? 'recorded' : 'unavailable';
|
|
2902
|
+
}
|
|
2903
|
+
prepareAgentReply(agent, input) {
|
|
1338
2904
|
this.assertActive();
|
|
1339
2905
|
const binding = agent === undefined ? undefined : this.deliveryStore.getBindingBySession(String(agent.session.id));
|
|
1340
2906
|
if (agent === undefined || binding === undefined || binding.status !== 'active'
|
|
@@ -1355,14 +2921,15 @@ export class AssistantDeliveryService extends Service {
|
|
|
1355
2921
|
throw new AssistantDeliveryError('missing-binding', 'Agent delivery principal is no longer active');
|
|
1356
2922
|
}
|
|
1357
2923
|
const format = this.replyFormat(binding.conversation, input.format) ?? 'plain';
|
|
1358
|
-
|
|
2924
|
+
const intent = { idempotencyKey, bindingId: binding.id,
|
|
1359
2925
|
target: { conversation: binding.conversation, principal: binding.principal }, text: input.text,
|
|
1360
2926
|
format,
|
|
1361
2927
|
...(input.modelPicker === undefined ? {} : { modelPicker: input.modelPicker }),
|
|
1362
2928
|
...(format !== 'permission-picker' || input.permissionPicker === undefined
|
|
1363
2929
|
? {}
|
|
1364
2930
|
: { permissionPicker: input.permissionPicker }),
|
|
1365
|
-
...(input.replyToEventId === undefined ? {} : { replyToEventId: input.replyToEventId }) }
|
|
2931
|
+
...(input.replyToEventId === undefined ? {} : { replyToEventId: input.replyToEventId }) };
|
|
2932
|
+
return Object.freeze({ binding, intent: Object.freeze(intent) });
|
|
1366
2933
|
}
|
|
1367
2934
|
history(agent, input) {
|
|
1368
2935
|
this.assertActive();
|
|
@@ -1419,8 +2986,17 @@ export class AssistantDeliveryService extends Service {
|
|
|
1419
2986
|
|| agent.session.header.agentPreset !== binding.agentPreset) {
|
|
1420
2987
|
throw new AssistantDeliveryError('missing-binding', 'Agent session has no authenticated active owner approval route');
|
|
1421
2988
|
}
|
|
1422
|
-
return Object.freeze({
|
|
1423
|
-
|
|
2989
|
+
return Object.freeze({
|
|
2990
|
+
routeVersion: 2,
|
|
2991
|
+
sourceId,
|
|
2992
|
+
bindingId: binding.id,
|
|
2993
|
+
bindingVersion: binding.version,
|
|
2994
|
+
bindingGeneration: binding.generation,
|
|
2995
|
+
workspace: binding.workspace,
|
|
2996
|
+
principal: externalPrincipalId(binding.principal),
|
|
2997
|
+
principalRecordId: owner.id,
|
|
2998
|
+
principalVersion: owner.version,
|
|
2999
|
+
});
|
|
1424
3000
|
}
|
|
1425
3001
|
async tick() {
|
|
1426
3002
|
this.assertActive();
|
|
@@ -1428,10 +3004,20 @@ export class AssistantDeliveryService extends Service {
|
|
|
1428
3004
|
await this.drainModelSelectionSettlements();
|
|
1429
3005
|
this.drainApprovalDispatches();
|
|
1430
3006
|
await this.outbound.tick();
|
|
3007
|
+
await this.drainDeliveryPresentations();
|
|
3008
|
+
await this.drainPreferenceProjections();
|
|
3009
|
+
await this.drainWorkflowTraces();
|
|
1431
3010
|
}
|
|
1432
3011
|
async whenIdle() {
|
|
1433
3012
|
this.assertActive();
|
|
1434
|
-
await Promise.all([
|
|
3013
|
+
await Promise.all([
|
|
3014
|
+
this.inbound.whenIdle(),
|
|
3015
|
+
this.outbound.whenIdle(),
|
|
3016
|
+
this.modelSelectionFlight,
|
|
3017
|
+
this.presentationFlight,
|
|
3018
|
+
this.preferenceProjectionFlight,
|
|
3019
|
+
this.workflowTraceFlight,
|
|
3020
|
+
]);
|
|
1435
3021
|
}
|
|
1436
3022
|
start() {
|
|
1437
3023
|
this.assertActive();
|
|
@@ -1591,10 +3177,183 @@ export class AssistantDeliveryService extends Service {
|
|
|
1591
3177
|
clearInterval(this.timer);
|
|
1592
3178
|
if (this.modelSelectionRetryTimer !== undefined)
|
|
1593
3179
|
clearTimeout(this.modelSelectionRetryTimer);
|
|
3180
|
+
if (this.workflowTraceRetryTimer !== undefined)
|
|
3181
|
+
clearTimeout(this.workflowTraceRetryTimer);
|
|
3182
|
+
if (this.preferenceProjectionRetryTimer !== undefined)
|
|
3183
|
+
clearTimeout(this.preferenceProjectionRetryTimer);
|
|
1594
3184
|
this.timer = undefined;
|
|
1595
3185
|
this.modelSelectionRetryTimer = undefined;
|
|
3186
|
+
this.workflowTraceRetryTimer = undefined;
|
|
3187
|
+
this.preferenceProjectionRetryTimer = undefined;
|
|
1596
3188
|
this.cancelModelSelectionWaiters();
|
|
1597
|
-
await Promise.all([
|
|
3189
|
+
await Promise.all([
|
|
3190
|
+
this.inbound.stop(),
|
|
3191
|
+
this.outbound.stop(),
|
|
3192
|
+
this.modelSelectionFlight,
|
|
3193
|
+
this.presentationFlight,
|
|
3194
|
+
this.preferenceProjectionFlight,
|
|
3195
|
+
this.workflowTraceFlight,
|
|
3196
|
+
]);
|
|
3197
|
+
}
|
|
3198
|
+
drainPreferenceProjections() {
|
|
3199
|
+
if (this.preferenceProjectionRetryTimer !== undefined) {
|
|
3200
|
+
clearTimeout(this.preferenceProjectionRetryTimer);
|
|
3201
|
+
this.preferenceProjectionRetryTimer = undefined;
|
|
3202
|
+
}
|
|
3203
|
+
if (this.preferenceProjectionFlight !== undefined)
|
|
3204
|
+
return this.preferenceProjectionFlight;
|
|
3205
|
+
const flight = this.runPreferenceProjection()
|
|
3206
|
+
.finally(() => {
|
|
3207
|
+
if (this.preferenceProjectionFlight === flight)
|
|
3208
|
+
this.preferenceProjectionFlight = undefined;
|
|
3209
|
+
});
|
|
3210
|
+
this.preferenceProjectionFlight = flight;
|
|
3211
|
+
return flight;
|
|
3212
|
+
}
|
|
3213
|
+
async runPreferenceProjection() {
|
|
3214
|
+
const sink = this.preferenceFeedbackSink;
|
|
3215
|
+
if (sink === undefined)
|
|
3216
|
+
return;
|
|
3217
|
+
let attempted = 0;
|
|
3218
|
+
while (attempted < 100) {
|
|
3219
|
+
const entries = this.deliveryStore.listPendingPreferenceProjections(100 - attempted);
|
|
3220
|
+
if (entries.length === 0)
|
|
3221
|
+
break;
|
|
3222
|
+
for (const entry of entries) {
|
|
3223
|
+
if (!this.active || this.preferenceFeedbackSink?.token !== sink.token)
|
|
3224
|
+
return;
|
|
3225
|
+
attempted += 1;
|
|
3226
|
+
try {
|
|
3227
|
+
if (sink.registration.appendSynchronously !== undefined) {
|
|
3228
|
+
this.deliveryStore.projectPreferenceProjectionUnderOwnerFence(entry, current => {
|
|
3229
|
+
if (!this.active || this.preferenceFeedbackSink?.token !== sink.token
|
|
3230
|
+
|| !registrationOwnedByPreference(sink.registration)) {
|
|
3231
|
+
throw Object.assign(new Error('stale Preference projection registration'), {
|
|
3232
|
+
code: 'stale-registration',
|
|
3233
|
+
});
|
|
3234
|
+
}
|
|
3235
|
+
const receipts = sink.registration.appendSynchronously(current.events);
|
|
3236
|
+
validatePreferenceProjectionReceipts(receipts, current.events);
|
|
3237
|
+
});
|
|
3238
|
+
}
|
|
3239
|
+
else {
|
|
3240
|
+
// Rolling-upgrade fallback. It still rechecks immediately before
|
|
3241
|
+
// sink I/O; current in-process Preference registrations use the
|
|
3242
|
+
// synchronous path above to close the remaining check/use window.
|
|
3243
|
+
if (!this.deliveryStore.preferenceProjectionHasCurrentOwner(entry))
|
|
3244
|
+
continue;
|
|
3245
|
+
const receipts = await Promise.resolve(sink.registration.append(entry.events));
|
|
3246
|
+
validatePreferenceProjectionReceipts(receipts, entry.events);
|
|
3247
|
+
if (!this.active || this.preferenceFeedbackSink?.token !== sink.token)
|
|
3248
|
+
return;
|
|
3249
|
+
this.deliveryStore.completePreferenceProjection({
|
|
3250
|
+
batchKey: entry.batchKey,
|
|
3251
|
+
payloadDigest: entry.payloadDigest,
|
|
3252
|
+
});
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
catch (error) {
|
|
3256
|
+
if (!this.active || this.preferenceFeedbackSink?.token !== sink.token)
|
|
3257
|
+
return;
|
|
3258
|
+
const now = Date.now();
|
|
3259
|
+
const retryAt = now + Math.min(this.config.retryMaxMs, this.config.retryBaseMs * (2 ** Math.min(30, entry.attemptCount)));
|
|
3260
|
+
const code = typeof error === 'object' && error !== null && 'code' in error
|
|
3261
|
+
&& typeof error.code === 'string'
|
|
3262
|
+
? `sink-${error.code}`.slice(0, 64)
|
|
3263
|
+
: 'sink-projection-failed';
|
|
3264
|
+
this.deliveryStore.deferPreferenceProjection({
|
|
3265
|
+
batchKey: entry.batchKey,
|
|
3266
|
+
payloadDigest: entry.payloadDigest,
|
|
3267
|
+
now,
|
|
3268
|
+
retryAt,
|
|
3269
|
+
failureCode: /^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/u.test(code)
|
|
3270
|
+
? code
|
|
3271
|
+
: 'sink-projection-failed',
|
|
3272
|
+
});
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
const nextRetryAt = this.deliveryStore.nextPreferenceProjectionAttemptAt();
|
|
3277
|
+
if (nextRetryAt !== undefined && this.preferenceFeedbackSink?.token === sink.token
|
|
3278
|
+
&& this.preferenceProjectionRetryTimer === undefined) {
|
|
3279
|
+
this.preferenceProjectionRetryTimer = setTimeout(() => {
|
|
3280
|
+
this.preferenceProjectionRetryTimer = undefined;
|
|
3281
|
+
if (this.active)
|
|
3282
|
+
void this.drainPreferenceProjections();
|
|
3283
|
+
}, Math.max(1, Math.min(2_147_483_647, nextRetryAt - Date.now())));
|
|
3284
|
+
this.preferenceProjectionRetryTimer.unref?.();
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
/** Host test/operator seam: project due trace revisions without waiting for the scheduler. */
|
|
3288
|
+
projectWorkflowTraces() {
|
|
3289
|
+
this.assertActive();
|
|
3290
|
+
return this.drainWorkflowTraces();
|
|
3291
|
+
}
|
|
3292
|
+
drainWorkflowTraces() {
|
|
3293
|
+
if (this.workflowTraceRetryTimer !== undefined) {
|
|
3294
|
+
clearTimeout(this.workflowTraceRetryTimer);
|
|
3295
|
+
this.workflowTraceRetryTimer = undefined;
|
|
3296
|
+
}
|
|
3297
|
+
if (this.workflowTraceFlight !== undefined)
|
|
3298
|
+
return this.workflowTraceFlight;
|
|
3299
|
+
const flight = this.runWorkflowTraceProjection()
|
|
3300
|
+
.finally(() => { if (this.workflowTraceFlight === flight)
|
|
3301
|
+
this.workflowTraceFlight = undefined; });
|
|
3302
|
+
this.workflowTraceFlight = flight;
|
|
3303
|
+
return flight;
|
|
3304
|
+
}
|
|
3305
|
+
async runWorkflowTraceProjection() {
|
|
3306
|
+
const registration = this.workflowTraceSink;
|
|
3307
|
+
if (registration === undefined)
|
|
3308
|
+
return;
|
|
3309
|
+
const entries = this.deliveryStore.listPendingWorkflowTraceRevisions(100);
|
|
3310
|
+
let nextRetryAt;
|
|
3311
|
+
for (const entry of entries) {
|
|
3312
|
+
if (!this.active || this.workflowTraceSink?.token !== registration.token)
|
|
3313
|
+
return;
|
|
3314
|
+
try {
|
|
3315
|
+
const candidate = await Promise.resolve(registration.sink.projectWorkflowTraceRevision(entry.revision));
|
|
3316
|
+
validateWorkflowTraceProjectionReceipt(candidate, entry.revision);
|
|
3317
|
+
if (!this.active || this.workflowTraceSink?.token !== registration.token)
|
|
3318
|
+
return;
|
|
3319
|
+
this.deliveryStore.completeWorkflowTraceRevision({
|
|
3320
|
+
subjectRef: entry.revision.subjectRef,
|
|
3321
|
+
version: entry.revision.version,
|
|
3322
|
+
digest: entry.revision.digest,
|
|
3323
|
+
});
|
|
3324
|
+
}
|
|
3325
|
+
catch (error) {
|
|
3326
|
+
if (!this.active || this.workflowTraceSink?.token !== registration.token)
|
|
3327
|
+
return;
|
|
3328
|
+
const now = Date.now();
|
|
3329
|
+
const retryDelay = Math.min(this.config.retryMaxMs, this.config.retryBaseMs * (2 ** Math.min(30, entry.attemptCount)));
|
|
3330
|
+
const retryAt = now + retryDelay;
|
|
3331
|
+
const code = typeof error === 'object' && error !== null && 'code' in error
|
|
3332
|
+
&& typeof error.code === 'string'
|
|
3333
|
+
? `sink-${error.code}`.slice(0, 64)
|
|
3334
|
+
: 'sink-projection-failed';
|
|
3335
|
+
this.deliveryStore.deferWorkflowTraceRevision({
|
|
3336
|
+
subjectRef: entry.revision.subjectRef,
|
|
3337
|
+
version: entry.revision.version,
|
|
3338
|
+
digest: entry.revision.digest,
|
|
3339
|
+
now,
|
|
3340
|
+
retryAt,
|
|
3341
|
+
failureCode: /^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/u.test(code)
|
|
3342
|
+
? code
|
|
3343
|
+
: 'sink-projection-failed',
|
|
3344
|
+
});
|
|
3345
|
+
nextRetryAt = Math.min(nextRetryAt ?? retryAt, retryAt);
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
if (nextRetryAt !== undefined && this.workflowTraceSink?.token === registration.token
|
|
3349
|
+
&& this.workflowTraceRetryTimer === undefined) {
|
|
3350
|
+
this.workflowTraceRetryTimer = setTimeout(() => {
|
|
3351
|
+
this.workflowTraceRetryTimer = undefined;
|
|
3352
|
+
if (this.active)
|
|
3353
|
+
void this.drainWorkflowTraces();
|
|
3354
|
+
}, Math.max(1, Math.min(2_147_483_647, nextRetryAt - Date.now())));
|
|
3355
|
+
this.workflowTraceRetryTimer.unref?.();
|
|
3356
|
+
}
|
|
1598
3357
|
}
|
|
1599
3358
|
resolveModelSelectionWaiters(operationId, result) {
|
|
1600
3359
|
const waiters = this.modelSelectionWaiters.get(operationId);
|
|
@@ -1614,10 +3373,20 @@ export class AssistantDeliveryService extends Service {
|
|
|
1614
3373
|
const dispatches = this.policy.listPendingApprovalDispatches(100, cursor.after);
|
|
1615
3374
|
for (const dispatch of dispatches) {
|
|
1616
3375
|
try {
|
|
3376
|
+
if (dispatch.routeVersion !== 2)
|
|
3377
|
+
continue;
|
|
1617
3378
|
this.enqueueApproval({
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
3379
|
+
route: {
|
|
3380
|
+
routeVersion: 2,
|
|
3381
|
+
sourceId: dispatch.sourceId,
|
|
3382
|
+
bindingId: dispatch.bindingId,
|
|
3383
|
+
bindingVersion: dispatch.bindingVersion,
|
|
3384
|
+
bindingGeneration: dispatch.bindingGeneration,
|
|
3385
|
+
workspace: dispatch.workspace,
|
|
3386
|
+
principal: dispatch.principal,
|
|
3387
|
+
principalRecordId: dispatch.principalRecordId,
|
|
3388
|
+
principalVersion: dispatch.principalVersion,
|
|
3389
|
+
},
|
|
1621
3390
|
idempotencyKey: `approval-card:${dispatch.proposalId}`,
|
|
1622
3391
|
text: dispatch.diff,
|
|
1623
3392
|
approval: {
|
|
@@ -1652,6 +3421,148 @@ export class AssistantDeliveryService extends Service {
|
|
|
1652
3421
|
throw error;
|
|
1653
3422
|
}
|
|
1654
3423
|
}
|
|
3424
|
+
drainDeliveryPresentations() {
|
|
3425
|
+
if (this.presentationFlight !== undefined)
|
|
3426
|
+
return this.presentationFlight;
|
|
3427
|
+
const flight = this.runDeliveryPresentations()
|
|
3428
|
+
.finally(() => { if (this.presentationFlight === flight)
|
|
3429
|
+
this.presentationFlight = undefined; });
|
|
3430
|
+
this.presentationFlight = flight;
|
|
3431
|
+
return flight;
|
|
3432
|
+
}
|
|
3433
|
+
async runDeliveryPresentations() {
|
|
3434
|
+
const attemptedPresentationKeys = [];
|
|
3435
|
+
for (let index = 0; index < this.config.maxConcurrency; index += 1) {
|
|
3436
|
+
const claimed = this.deliveryStore.claimDeliveryPresentation({
|
|
3437
|
+
ownerId: this.ownerId,
|
|
3438
|
+
leaseMs: this.config.leaseMs,
|
|
3439
|
+
excludePresentationKeys: attemptedPresentationKeys,
|
|
3440
|
+
});
|
|
3441
|
+
if (claimed === undefined)
|
|
3442
|
+
return;
|
|
3443
|
+
const { presentation, fencingToken } = claimed;
|
|
3444
|
+
attemptedPresentationKeys.push(presentation.presentationKey);
|
|
3445
|
+
const original = this.deliveryStore.getOutboxByIdempotencyKey(presentation.originalOutboxIdempotencyKey);
|
|
3446
|
+
const finishFailure = (failureCode, terminal = false) => {
|
|
3447
|
+
// Presentation is a projection of an already-authoritative terminal
|
|
3448
|
+
// domain receipt. A transient provider failure must never turn that
|
|
3449
|
+
// receipt into a permanent lie simply because the normal message-send
|
|
3450
|
+
// retry budget was exhausted. Only an immutable original or revoked
|
|
3451
|
+
// authority can terminally fence the projection.
|
|
3452
|
+
const outcome = terminal ? 'dead' : 'retry_wait';
|
|
3453
|
+
const retryDelay = Math.min(this.config.retryMaxMs, this.config.retryBaseMs * (2 ** Math.min(30, Math.max(0, presentation.attemptCount - 1))));
|
|
3454
|
+
try {
|
|
3455
|
+
this.deliveryStore.finishDeliveryPresentation({
|
|
3456
|
+
presentationKey: presentation.presentationKey,
|
|
3457
|
+
revision: presentation.revision,
|
|
3458
|
+
ownerId: this.ownerId,
|
|
3459
|
+
fencingToken,
|
|
3460
|
+
outcome,
|
|
3461
|
+
failureCode,
|
|
3462
|
+
...(outcome === 'retry_wait' ? { nextAttemptAt: Date.now() + retryDelay } : {}),
|
|
3463
|
+
});
|
|
3464
|
+
}
|
|
3465
|
+
catch (error) {
|
|
3466
|
+
// A newer desired revision superseded this provider attempt. Its
|
|
3467
|
+
// pending row is authoritative and must not be overwritten by the
|
|
3468
|
+
// older completion.
|
|
3469
|
+
if (error instanceof DeliveryStoreError && error.code === 'stale-fence')
|
|
3470
|
+
return;
|
|
3471
|
+
throw error;
|
|
3472
|
+
}
|
|
3473
|
+
};
|
|
3474
|
+
if (original === undefined) {
|
|
3475
|
+
// The domain can settle before the Policy dispatch scanner creates the
|
|
3476
|
+
// card. Preserve the desired state without exhausting retries.
|
|
3477
|
+
this.deliveryStore.finishDeliveryPresentation({
|
|
3478
|
+
presentationKey: presentation.presentationKey,
|
|
3479
|
+
revision: presentation.revision,
|
|
3480
|
+
ownerId: this.ownerId,
|
|
3481
|
+
fencingToken,
|
|
3482
|
+
outcome: 'retry_wait',
|
|
3483
|
+
failureCode: 'original-message-not-created',
|
|
3484
|
+
nextAttemptAt: Date.now() + this.config.retryBaseMs,
|
|
3485
|
+
});
|
|
3486
|
+
continue;
|
|
3487
|
+
}
|
|
3488
|
+
if (original.status === 'dead') {
|
|
3489
|
+
finishFailure('original-message-dead', true);
|
|
3490
|
+
continue;
|
|
3491
|
+
}
|
|
3492
|
+
if (original.providerMessageId === undefined) {
|
|
3493
|
+
this.deliveryStore.finishDeliveryPresentation({
|
|
3494
|
+
presentationKey: presentation.presentationKey,
|
|
3495
|
+
revision: presentation.revision,
|
|
3496
|
+
ownerId: this.ownerId,
|
|
3497
|
+
fencingToken,
|
|
3498
|
+
outcome: 'retry_wait',
|
|
3499
|
+
failureCode: 'original-provider-message-pending',
|
|
3500
|
+
nextAttemptAt: Date.now() + this.config.retryBaseMs,
|
|
3501
|
+
});
|
|
3502
|
+
continue;
|
|
3503
|
+
}
|
|
3504
|
+
if (presentation.presentation.kind === 'automation-incident') {
|
|
3505
|
+
const route = this.deliveryStore.validatePresentationOwnerRoute(original, this.ownerRouteGuard);
|
|
3506
|
+
if (route.kind === 'deferred') {
|
|
3507
|
+
finishFailure(route.failureCode);
|
|
3508
|
+
continue;
|
|
3509
|
+
}
|
|
3510
|
+
if (route.kind !== 'authorized') {
|
|
3511
|
+
finishFailure(route.kind === 'denied'
|
|
3512
|
+
? route.failureCode
|
|
3513
|
+
: 'presentation-owner-route-required', true);
|
|
3514
|
+
continue;
|
|
3515
|
+
}
|
|
3516
|
+
}
|
|
3517
|
+
else {
|
|
3518
|
+
const binding = this.deliveryStore.getBinding(original.intent.bindingId);
|
|
3519
|
+
const principal = binding === undefined ? undefined : this.deliveryStore.getPrincipal(binding.principal);
|
|
3520
|
+
if (binding?.status !== 'active' || principal?.status !== 'active' || principal.role !== 'owner') {
|
|
3521
|
+
finishFailure('presentation-route-revoked', true);
|
|
3522
|
+
continue;
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
const adapter = this.registry.get(original.intent.target.conversation.channel, original.intent.target.conversation.account);
|
|
3526
|
+
if (adapter?.updatePresentation === undefined) {
|
|
3527
|
+
finishFailure('presentation-adapter-unavailable');
|
|
3528
|
+
continue;
|
|
3529
|
+
}
|
|
3530
|
+
const controller = new AbortController();
|
|
3531
|
+
const timeout = setTimeout(() => controller.abort(new Error('delivery presentation lease elapsed')), this.config.leaseMs);
|
|
3532
|
+
timeout.unref?.();
|
|
3533
|
+
let updateError;
|
|
3534
|
+
try {
|
|
3535
|
+
await adapter.updatePresentation(original.providerMessageId, presentation.presentation, controller.signal);
|
|
3536
|
+
}
|
|
3537
|
+
catch (error) {
|
|
3538
|
+
updateError = error;
|
|
3539
|
+
}
|
|
3540
|
+
finally {
|
|
3541
|
+
clearTimeout(timeout);
|
|
3542
|
+
}
|
|
3543
|
+
if (updateError !== undefined) {
|
|
3544
|
+
const code = updateError instanceof Error
|
|
3545
|
+
? `presentation-${updateError.name.normalize('NFC').toLowerCase().replaceAll(/[^a-z0-9]+/gu, '-').slice(0, 80)}`
|
|
3546
|
+
: 'presentation-unknown-error';
|
|
3547
|
+
finishFailure(code);
|
|
3548
|
+
continue;
|
|
3549
|
+
}
|
|
3550
|
+
try {
|
|
3551
|
+
this.deliveryStore.finishDeliveryPresentation({
|
|
3552
|
+
presentationKey: presentation.presentationKey,
|
|
3553
|
+
revision: presentation.revision,
|
|
3554
|
+
ownerId: this.ownerId,
|
|
3555
|
+
fencingToken,
|
|
3556
|
+
outcome: 'presented',
|
|
3557
|
+
providerMessageId: original.providerMessageId,
|
|
3558
|
+
});
|
|
3559
|
+
}
|
|
3560
|
+
catch (error) {
|
|
3561
|
+
if (!(error instanceof DeliveryStoreError) || error.code !== 'stale-fence')
|
|
3562
|
+
throw error;
|
|
3563
|
+
}
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
1655
3566
|
drainModelSelectionSettlements() {
|
|
1656
3567
|
if (this.modelSelectionRetryTimer !== undefined) {
|
|
1657
3568
|
clearTimeout(this.modelSelectionRetryTimer);
|