@dsh-enhanced/assistant-delivery 0.1.7 → 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 +89 -10
- package/cordis.patch.yml +1 -0
- package/lib/agent-runtime.d.ts +18 -4
- package/lib/agent-runtime.d.ts.map +1 -1
- package/lib/agent-runtime.js +240 -14
- 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 +48 -2
- package/lib/feedback-command.d.ts.map +1 -1
- package/lib/feedback-command.js +226 -10
- package/lib/feedback-command.js.map +1 -1
- 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 +142 -10
- package/lib/service.d.ts.map +1 -1
- package/lib/service.js +1841 -62
- 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 +364 -2
- 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,11 +431,14 @@ 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();
|
|
@@ -176,13 +446,34 @@ export class AssistantDeliveryService extends Service {
|
|
|
176
446
|
toolApprovalControllers = new Set();
|
|
177
447
|
modelSelectionWaiters = new Map();
|
|
178
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();
|
|
179
457
|
modelSelectionFlight;
|
|
458
|
+
presentationFlight;
|
|
459
|
+
workflowTraceFlight;
|
|
460
|
+
preferenceProjectionFlight;
|
|
461
|
+
workflowTraceRetryTimer;
|
|
462
|
+
preferenceProjectionRetryTimer;
|
|
180
463
|
modelSelectionRetryTimer;
|
|
181
464
|
runtime;
|
|
182
465
|
timer;
|
|
183
466
|
active = true;
|
|
184
467
|
constructor(ctx, input) {
|
|
185
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;
|
|
186
477
|
let config;
|
|
187
478
|
try {
|
|
188
479
|
config = AssistantDeliveryService.Config(input);
|
|
@@ -198,9 +489,40 @@ export class AssistantDeliveryService extends Service {
|
|
|
198
489
|
throw new Error('assistant-delivery: assistantPolicy service is required');
|
|
199
490
|
mkdirSync(config.spoolPath, { recursive: true, mode: 0o700 });
|
|
200
491
|
chmodSync(config.spoolPath, 0o700);
|
|
201
|
-
|
|
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]));
|
|
202
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);
|
|
203
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
|
+
});
|
|
204
526
|
this.registry = new DeliveryAdapterRegistry({
|
|
205
527
|
accept: async (envelope) => {
|
|
206
528
|
const result = await this.acceptInbound(envelope);
|
|
@@ -220,7 +542,9 @@ export class AssistantDeliveryService extends Service {
|
|
|
220
542
|
});
|
|
221
543
|
this.outbound = new DeliveryCoordinator({ store: this.deliveryStore, registry: this.registry,
|
|
222
544
|
ownerId: this.ownerId, leaseMs: config.leaseMs, maxAttempts: config.maxAttempts,
|
|
223
|
-
maxConcurrency: config.maxConcurrency, retryBaseMs: config.retryBaseMs, retryMaxMs: config.retryMaxMs
|
|
545
|
+
maxConcurrency: config.maxConcurrency, retryBaseMs: config.retryBaseMs, retryMaxMs: config.retryMaxMs,
|
|
546
|
+
ownerRouteGuard: this.ownerRouteGuard,
|
|
547
|
+
});
|
|
224
548
|
this.inbound = new InboundCoordinator({ store: this.deliveryStore, processor: () => this.runtime,
|
|
225
549
|
ownerId: this.ownerId, leaseMs: config.leaseMs, maxAttempts: config.maxAttempts,
|
|
226
550
|
maxConcurrency: config.maxConcurrency, retryBaseMs: config.retryBaseMs, retryMaxMs: config.retryMaxMs });
|
|
@@ -240,6 +564,10 @@ export class AssistantDeliveryService extends Service {
|
|
|
240
564
|
isOwnerFeedbackController: (binding, envelope) => this.isOwnerFeedbackController(binding, envelope),
|
|
241
565
|
authorizeOwnerPreferenceFeedback: (binding, envelope, selections) => this.authorizeOwnerPreferenceFeedback(binding, envelope, selections),
|
|
242
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),
|
|
243
571
|
authorizePermissionReply: (binding, envelope) => this.authorizePermissionReply(binding, envelope),
|
|
244
572
|
beginModelCommand: conversation => this.deliveryStore.beginModelCommand(conversation),
|
|
245
573
|
commitModelCommand: input => this.deliveryStore.commitModelCommand(input),
|
|
@@ -262,27 +590,45 @@ export class AssistantDeliveryService extends Service {
|
|
|
262
590
|
...(reply.permissionPicker === undefined ? {} : { permissionPicker: reply.permissionPicker }),
|
|
263
591
|
});
|
|
264
592
|
},
|
|
265
|
-
reply: (agent, eventId, reply) => {
|
|
266
|
-
this.reply(agent, {
|
|
267
|
-
idempotencyKey: `inbound:${eventId}:reply`,
|
|
268
|
-
text: reply.text,
|
|
269
|
-
replyToEventId: eventId,
|
|
270
|
-
...(reply.format === undefined ? {} : { format: reply.format }),
|
|
271
|
-
...(reply.modelPicker === undefined ? {} : { modelPicker: reply.modelPicker }),
|
|
272
|
-
...(reply.permissionPicker === undefined ? {} : { permissionPicker: reply.permissionPicker }),
|
|
273
|
-
});
|
|
274
|
-
},
|
|
275
593
|
}));
|
|
276
594
|
void this.drainModelSelectionSettlements();
|
|
277
595
|
return unregister;
|
|
278
596
|
});
|
|
279
597
|
ctx.inject(['approval'], approvalCtx => approvalCtx.on('approval/request', (request, next) => this.requestToolApproval(approvalCtx, request, next)));
|
|
280
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
|
+
});
|
|
281
622
|
if (config.schedulerEnabled)
|
|
282
623
|
this.start();
|
|
283
624
|
ctx.effect(() => async () => {
|
|
284
625
|
this.active = false;
|
|
626
|
+
trustedDeliveryPreferenceProducers.delete(this);
|
|
285
627
|
this.preferenceFeedbackSink = undefined;
|
|
628
|
+
this.evaluationSink = undefined;
|
|
629
|
+
this.workflowTraceSink = undefined;
|
|
630
|
+
this.automationPresentationBinding?.dispose();
|
|
631
|
+
this.evolutionPresentationBinding?.dispose();
|
|
286
632
|
this.cancelModelSelectionWaiters();
|
|
287
633
|
for (const controller of this.toolApprovalControllers) {
|
|
288
634
|
controller.abort(new Error('assistant-delivery is stopping'));
|
|
@@ -748,16 +1094,26 @@ export class AssistantDeliveryService extends Service {
|
|
|
748
1094
|
}
|
|
749
1095
|
resolveDeadLetter(input) {
|
|
750
1096
|
this.assertActive();
|
|
751
|
-
|
|
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}` },
|
|
752
1108
|
action: 'delivery.resolve', resource: { kind: 'message', id: `${input.kind}:${input.id}` },
|
|
753
|
-
context: { initiator: 'foreground' } }, { idempotencyKey: `delivery-resolve:${
|
|
1109
|
+
context: { initiator: 'foreground' } }, { idempotencyKey: `delivery-resolve:${operationDigest}` });
|
|
754
1110
|
if (decision.effect !== 'allow')
|
|
755
1111
|
throw policyDenied(decision);
|
|
756
1112
|
return input.kind === 'inbox'
|
|
757
1113
|
? this.deliveryStore.resolveInbox({ inboxId: input.id, expectedAttemptCount: input.expectedAttemptCount,
|
|
758
|
-
resolution: input.resolution })
|
|
1114
|
+
resolution: input.resolution, operatorId })
|
|
759
1115
|
: this.deliveryStore.resolveOutbox({ outboxId: input.id, expectedAttemptCount: input.expectedAttemptCount,
|
|
760
|
-
resolution: input.resolution });
|
|
1116
|
+
resolution: input.resolution, operatorId });
|
|
761
1117
|
}
|
|
762
1118
|
registerInboundRuntime(runtime) {
|
|
763
1119
|
this.assertActive();
|
|
@@ -777,34 +1133,452 @@ export class AssistantDeliveryService extends Service {
|
|
|
777
1133
|
this.assertActive();
|
|
778
1134
|
return this.registry.register(adapter);
|
|
779
1135
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
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) {
|
|
786
1217
|
this.assertActive();
|
|
787
|
-
if (
|
|
788
|
-
|
|
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');
|
|
789
1226
|
}
|
|
790
1227
|
if (this.preferenceFeedbackSink !== undefined) {
|
|
791
|
-
|
|
1228
|
+
if (this.preferenceFeedbackSink.registration === registration) {
|
|
1229
|
+
return this.preferenceFeedbackSink.dispose;
|
|
1230
|
+
}
|
|
1231
|
+
throw new AssistantDeliveryError('runtime-conflict', 'trusted Preference sink is already registered');
|
|
792
1232
|
}
|
|
793
|
-
const token = Symbol('
|
|
794
|
-
this.preferenceFeedbackSink = Object.freeze({ token, listener });
|
|
1233
|
+
const token = Symbol('assistant-delivery.trusted-preference');
|
|
795
1234
|
let active = true;
|
|
796
|
-
|
|
1235
|
+
const dispose = () => {
|
|
797
1236
|
if (!active)
|
|
798
1237
|
return;
|
|
799
1238
|
active = false;
|
|
800
1239
|
if (this.preferenceFeedbackSink?.token === token)
|
|
801
1240
|
this.preferenceFeedbackSink = undefined;
|
|
802
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
|
+
}
|
|
803
1426
|
}
|
|
804
1427
|
health() {
|
|
805
1428
|
this.assertActive();
|
|
806
1429
|
return { ...this.deliveryStore.health(), adapters: this.registry.size() };
|
|
807
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
|
+
}
|
|
808
1582
|
isInboundAuthorized(binding, envelope) {
|
|
809
1583
|
if (!this.active)
|
|
810
1584
|
return false;
|
|
@@ -876,6 +1650,16 @@ export class AssistantDeliveryService extends Service {
|
|
|
876
1650
|
context: { initiator: 'external' },
|
|
877
1651
|
}).effect === 'allow';
|
|
878
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
|
+
}
|
|
879
1663
|
authorizeOwnerPreferenceFeedback(binding, envelope, selections) {
|
|
880
1664
|
if (!this.isOwnerFeedbackController(binding, envelope)
|
|
881
1665
|
|| !Array.isArray(selections) || selections.length < 1 || selections.length > 16)
|
|
@@ -884,6 +1668,9 @@ export class AssistantDeliveryService extends Service {
|
|
|
884
1668
|
if (inbox?.status !== 'claimed' || inbox.bindingId !== binding.id
|
|
885
1669
|
|| JSON.stringify(inbox.envelope) !== JSON.stringify(envelope))
|
|
886
1670
|
return undefined;
|
|
1671
|
+
const principalLineage = this.ownerLineageForBinding(binding);
|
|
1672
|
+
if (principalLineage === undefined)
|
|
1673
|
+
return undefined;
|
|
887
1674
|
const subject = {
|
|
888
1675
|
kind: 'external',
|
|
889
1676
|
id: externalPrincipalId(binding.principal),
|
|
@@ -916,7 +1703,29 @@ export class AssistantDeliveryService extends Service {
|
|
|
916
1703
|
}
|
|
917
1704
|
// Provider clocks are not authoritative. The locally persisted Inbox time
|
|
918
1705
|
// is bounded by the same Host clock as Preference Learning's retention gate.
|
|
919
|
-
|
|
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
|
+
});
|
|
920
1729
|
}
|
|
921
1730
|
preferenceSignalAuthorizationKey(binding, envelope, resourceId, candidateValue) {
|
|
922
1731
|
const digest = createHash('sha256').update('assistant-delivery-preference-authorization-v2\0')
|
|
@@ -933,26 +1742,429 @@ export class AssistantDeliveryService extends Service {
|
|
|
933
1742
|
return `delivery-pref-auth:${digest}`;
|
|
934
1743
|
}
|
|
935
1744
|
async dispatchPreferenceFeedback(events) {
|
|
936
|
-
|
|
937
|
-
|
|
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))
|
|
938
2101
|
return 'unavailable';
|
|
2102
|
+
let proof;
|
|
939
2103
|
try {
|
|
940
|
-
|
|
941
|
-
|
|
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) {
|
|
942
2160
|
return 'unknown';
|
|
943
|
-
const expected = new Set(events.map(event => event.idempotencyKey));
|
|
944
|
-
const received = new Set();
|
|
945
|
-
for (const receipt of receipts) {
|
|
946
|
-
if (receipt?.status !== 'recorded'
|
|
947
|
-
|| typeof receipt.idempotencyKey !== 'string'
|
|
948
|
-
|| !expected.has(receipt.idempotencyKey)
|
|
949
|
-
|| received.has(receipt.idempotencyKey))
|
|
950
|
-
return 'unknown';
|
|
951
|
-
received.add(receipt.idempotencyKey);
|
|
952
2161
|
}
|
|
953
|
-
return
|
|
2162
|
+
return 'recorded';
|
|
954
2163
|
}
|
|
955
|
-
catch {
|
|
2164
|
+
catch (error) {
|
|
2165
|
+
if (typeof error === 'object' && error !== null && 'code' in error
|
|
2166
|
+
&& error.code === 'idempotency-conflict')
|
|
2167
|
+
return 'conflict';
|
|
956
2168
|
return 'unknown';
|
|
957
2169
|
}
|
|
958
2170
|
}
|
|
@@ -1360,6 +2572,9 @@ export class AssistantDeliveryService extends Service {
|
|
|
1360
2572
|
}
|
|
1361
2573
|
enqueueBackground(input) {
|
|
1362
2574
|
this.assertActive();
|
|
2575
|
+
if (containsReservedLearningMetadata(input.metadata)) {
|
|
2576
|
+
throw new AssistantDeliveryError('runtime-conflict', 'reserved learning metadata requires the typed Automation result seam');
|
|
2577
|
+
}
|
|
1363
2578
|
const binding = this.deliveryStore.getBinding(input.bindingId);
|
|
1364
2579
|
if (binding === undefined || binding.status !== 'active' || !this.hasActivePrincipal(binding)) {
|
|
1365
2580
|
throw new AssistantDeliveryError('missing-binding', 'delivery binding does not exist or is revoked');
|
|
@@ -1377,15 +2592,198 @@ export class AssistantDeliveryService extends Service {
|
|
|
1377
2592
|
}
|
|
1378
2593
|
return this.deliveryStore.enqueue({ idempotencyKey: input.idempotencyKey, bindingId: binding.id,
|
|
1379
2594
|
target: { conversation: binding.conversation, principal: binding.principal }, text: input.text,
|
|
1380
|
-
format: input.format ?? 'plain'
|
|
2595
|
+
format: input.format ?? 'plain',
|
|
2596
|
+
...(input.metadata === undefined ? {} : { metadata: input.metadata }) });
|
|
1381
2597
|
}
|
|
1382
|
-
|
|
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) {
|
|
1383
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
|
+
}
|
|
1384
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);
|
|
1385
2783
|
if (binding === undefined || binding.status !== 'active') {
|
|
1386
2784
|
throw new AssistantDeliveryError('missing-binding', 'delivery binding does not exist or is revoked');
|
|
1387
2785
|
}
|
|
1388
|
-
if (binding.workspace !== input.workspace) {
|
|
2786
|
+
if (binding.workspace !== input.route.workspace) {
|
|
1389
2787
|
throw new AssistantDeliveryError('missing-binding', 'delivery workspace does not match the active binding');
|
|
1390
2788
|
}
|
|
1391
2789
|
const owner = this.deliveryStore.getPrincipal(binding.principal);
|
|
@@ -1401,14 +2799,23 @@ export class AssistantDeliveryService extends Service {
|
|
|
1401
2799
|
|| createHash('sha256').update(input.text).digest('hex') !== proposal.diffHash) {
|
|
1402
2800
|
throw new AssistantDeliveryError('missing-binding', 'approval card does not match its active owner and Policy proposal');
|
|
1403
2801
|
}
|
|
1404
|
-
const decision = this.policy.authorize({ subject: { kind: 'background', id: input.sourceId,
|
|
1405
|
-
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',
|
|
1406
2804
|
resource: { kind: 'message', id: binding.id }, context: { initiator: 'background' } }, { idempotencyKey: `message-approval:${input.idempotencyKey}` });
|
|
1407
2805
|
if (decision.effect !== 'allow')
|
|
1408
2806
|
throw policyDenied(decision);
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
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
|
+
}
|
|
1412
2819
|
}
|
|
1413
2820
|
replyCommand(bindingInput, input) {
|
|
1414
2821
|
this.assertActive();
|
|
@@ -1467,6 +2874,33 @@ export class AssistantDeliveryService extends Service {
|
|
|
1467
2874
|
return adapter?.capabilities.formats.includes('markdown') === true ? 'markdown' : 'plain';
|
|
1468
2875
|
}
|
|
1469
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) {
|
|
1470
2904
|
this.assertActive();
|
|
1471
2905
|
const binding = agent === undefined ? undefined : this.deliveryStore.getBindingBySession(String(agent.session.id));
|
|
1472
2906
|
if (agent === undefined || binding === undefined || binding.status !== 'active'
|
|
@@ -1487,14 +2921,15 @@ export class AssistantDeliveryService extends Service {
|
|
|
1487
2921
|
throw new AssistantDeliveryError('missing-binding', 'Agent delivery principal is no longer active');
|
|
1488
2922
|
}
|
|
1489
2923
|
const format = this.replyFormat(binding.conversation, input.format) ?? 'plain';
|
|
1490
|
-
|
|
2924
|
+
const intent = { idempotencyKey, bindingId: binding.id,
|
|
1491
2925
|
target: { conversation: binding.conversation, principal: binding.principal }, text: input.text,
|
|
1492
2926
|
format,
|
|
1493
2927
|
...(input.modelPicker === undefined ? {} : { modelPicker: input.modelPicker }),
|
|
1494
2928
|
...(format !== 'permission-picker' || input.permissionPicker === undefined
|
|
1495
2929
|
? {}
|
|
1496
2930
|
: { permissionPicker: input.permissionPicker }),
|
|
1497
|
-
...(input.replyToEventId === undefined ? {} : { replyToEventId: input.replyToEventId }) }
|
|
2931
|
+
...(input.replyToEventId === undefined ? {} : { replyToEventId: input.replyToEventId }) };
|
|
2932
|
+
return Object.freeze({ binding, intent: Object.freeze(intent) });
|
|
1498
2933
|
}
|
|
1499
2934
|
history(agent, input) {
|
|
1500
2935
|
this.assertActive();
|
|
@@ -1551,8 +2986,17 @@ export class AssistantDeliveryService extends Service {
|
|
|
1551
2986
|
|| agent.session.header.agentPreset !== binding.agentPreset) {
|
|
1552
2987
|
throw new AssistantDeliveryError('missing-binding', 'Agent session has no authenticated active owner approval route');
|
|
1553
2988
|
}
|
|
1554
|
-
return Object.freeze({
|
|
1555
|
-
|
|
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
|
+
});
|
|
1556
3000
|
}
|
|
1557
3001
|
async tick() {
|
|
1558
3002
|
this.assertActive();
|
|
@@ -1560,10 +3004,20 @@ export class AssistantDeliveryService extends Service {
|
|
|
1560
3004
|
await this.drainModelSelectionSettlements();
|
|
1561
3005
|
this.drainApprovalDispatches();
|
|
1562
3006
|
await this.outbound.tick();
|
|
3007
|
+
await this.drainDeliveryPresentations();
|
|
3008
|
+
await this.drainPreferenceProjections();
|
|
3009
|
+
await this.drainWorkflowTraces();
|
|
1563
3010
|
}
|
|
1564
3011
|
async whenIdle() {
|
|
1565
3012
|
this.assertActive();
|
|
1566
|
-
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
|
+
]);
|
|
1567
3021
|
}
|
|
1568
3022
|
start() {
|
|
1569
3023
|
this.assertActive();
|
|
@@ -1723,10 +3177,183 @@ export class AssistantDeliveryService extends Service {
|
|
|
1723
3177
|
clearInterval(this.timer);
|
|
1724
3178
|
if (this.modelSelectionRetryTimer !== undefined)
|
|
1725
3179
|
clearTimeout(this.modelSelectionRetryTimer);
|
|
3180
|
+
if (this.workflowTraceRetryTimer !== undefined)
|
|
3181
|
+
clearTimeout(this.workflowTraceRetryTimer);
|
|
3182
|
+
if (this.preferenceProjectionRetryTimer !== undefined)
|
|
3183
|
+
clearTimeout(this.preferenceProjectionRetryTimer);
|
|
1726
3184
|
this.timer = undefined;
|
|
1727
3185
|
this.modelSelectionRetryTimer = undefined;
|
|
3186
|
+
this.workflowTraceRetryTimer = undefined;
|
|
3187
|
+
this.preferenceProjectionRetryTimer = undefined;
|
|
1728
3188
|
this.cancelModelSelectionWaiters();
|
|
1729
|
-
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
|
+
}
|
|
1730
3357
|
}
|
|
1731
3358
|
resolveModelSelectionWaiters(operationId, result) {
|
|
1732
3359
|
const waiters = this.modelSelectionWaiters.get(operationId);
|
|
@@ -1746,10 +3373,20 @@ export class AssistantDeliveryService extends Service {
|
|
|
1746
3373
|
const dispatches = this.policy.listPendingApprovalDispatches(100, cursor.after);
|
|
1747
3374
|
for (const dispatch of dispatches) {
|
|
1748
3375
|
try {
|
|
3376
|
+
if (dispatch.routeVersion !== 2)
|
|
3377
|
+
continue;
|
|
1749
3378
|
this.enqueueApproval({
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
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
|
+
},
|
|
1753
3390
|
idempotencyKey: `approval-card:${dispatch.proposalId}`,
|
|
1754
3391
|
text: dispatch.diff,
|
|
1755
3392
|
approval: {
|
|
@@ -1784,6 +3421,148 @@ export class AssistantDeliveryService extends Service {
|
|
|
1784
3421
|
throw error;
|
|
1785
3422
|
}
|
|
1786
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
|
+
}
|
|
1787
3566
|
drainModelSelectionSettlements() {
|
|
1788
3567
|
if (this.modelSelectionRetryTimer !== undefined) {
|
|
1789
3568
|
clearTimeout(this.modelSelectionRetryTimer);
|