@dsh-enhanced/assistant-delivery 0.1.7 → 0.1.14

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.
Files changed (59) hide show
  1. package/README.md +89 -10
  2. package/cordis.patch.yml +1 -0
  3. package/lib/agent-runtime.d.ts +18 -4
  4. package/lib/agent-runtime.d.ts.map +1 -1
  5. package/lib/agent-runtime.js +240 -14
  6. package/lib/agent-runtime.js.map +1 -1
  7. package/lib/canonical.d.ts +11 -1
  8. package/lib/canonical.d.ts.map +1 -1
  9. package/lib/canonical.js +107 -0
  10. package/lib/canonical.js.map +1 -1
  11. package/lib/coordinator.d.ts +6 -1
  12. package/lib/coordinator.d.ts.map +1 -1
  13. package/lib/coordinator.js +63 -24
  14. package/lib/coordinator.js.map +1 -1
  15. package/lib/feedback-command.d.ts +48 -2
  16. package/lib/feedback-command.d.ts.map +1 -1
  17. package/lib/feedback-command.js +226 -10
  18. package/lib/feedback-command.js.map +1 -1
  19. package/lib/index.d.ts +5 -3
  20. package/lib/index.d.ts.map +1 -1
  21. package/lib/index.js +5 -3
  22. package/lib/index.js.map +1 -1
  23. package/lib/learning-command.d.ts +40 -0
  24. package/lib/learning-command.d.ts.map +1 -0
  25. package/lib/learning-command.js +57 -0
  26. package/lib/learning-command.js.map +1 -0
  27. package/lib/service.d.ts +142 -10
  28. package/lib/service.d.ts.map +1 -1
  29. package/lib/service.js +1841 -62
  30. package/lib/service.js.map +1 -1
  31. package/lib/session-commands.d.ts +14 -0
  32. package/lib/session-commands.d.ts.map +1 -1
  33. package/lib/session-commands.js +30 -0
  34. package/lib/session-commands.js.map +1 -1
  35. package/lib/sqlite.d.ts +1 -1
  36. package/lib/sqlite.d.ts.map +1 -1
  37. package/lib/sqlite.js +648 -20
  38. package/lib/sqlite.js.map +1 -1
  39. package/lib/store.d.ts +375 -3
  40. package/lib/store.d.ts.map +1 -1
  41. package/lib/store.js +2956 -74
  42. package/lib/store.js.map +1 -1
  43. package/lib/types.d.ts +364 -2
  44. package/lib/types.d.ts.map +1 -1
  45. package/lib/types.js +9 -1
  46. package/lib/types.js.map +1 -1
  47. package/lib/version.d.ts +1 -1
  48. package/lib/version.d.ts.map +1 -1
  49. package/lib/version.js +1 -1
  50. package/lib/version.js.map +1 -1
  51. package/lib/workflow-auto-producer.d.ts +37 -0
  52. package/lib/workflow-auto-producer.d.ts.map +1 -0
  53. package/lib/workflow-auto-producer.js +58 -0
  54. package/lib/workflow-auto-producer.js.map +1 -0
  55. package/lib/workflow-command.d.ts +16 -0
  56. package/lib/workflow-command.d.ts.map +1 -0
  57. package/lib/workflow-command.js +52 -0
  58. package/lib/workflow-command.js.map +1 -0
  59. 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
- this.config = config;
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
- const decision = this.policy.authorize({ subject: { kind: 'external', id: `local:${input.operatorId}` },
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:${input.kind}:${input.id}:${input.expectedAttemptCount}:${input.resolution}` });
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
- * Trusted Host registration seam for the one authoritative, durable sink.
782
- * It is intentionally not an Agent tool. The receipt contract prevents a
783
- * no-op observer from being mistaken for persistence.
784
- */
785
- subscribePreferenceFeedback(listener) {
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 (typeof listener !== 'function') {
788
- throw new TypeError('assistant-delivery: preference feedback listener must be a function');
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
- throw new Error('assistant-delivery: a preference feedback sink is already registered');
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('preference-feedback-sink');
794
- this.preferenceFeedbackSink = Object.freeze({ token, listener });
1233
+ const token = Symbol('assistant-delivery.trusted-preference');
795
1234
  let active = true;
796
- return () => {
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
- return Object.freeze({ occurredAt: inbox.receivedAt });
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
- const registration = this.preferenceFeedbackSink;
937
- if (registration === undefined)
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
- const receipts = await registration.listener(events);
941
- if (!Array.isArray(receipts) || receipts.length !== events.length)
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 received.size === expected.size ? 'recorded' : 'unknown';
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
- enqueueApproval(input) {
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
- return this.deliveryStore.enqueue({ idempotencyKey: input.idempotencyKey, bindingId: binding.id,
1410
- target: { conversation: binding.conversation, principal: binding.principal }, text: input.text,
1411
- format: 'approval', approval: input.approval });
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
- return this.deliveryStore.enqueue({ idempotencyKey, bindingId: binding.id,
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({ sourceId, bindingId: binding.id, workspace: binding.workspace,
1555
- principal: externalPrincipalId(binding.principal) });
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([this.inbound.whenIdle(), this.outbound.whenIdle(), this.modelSelectionFlight]);
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([this.inbound.stop(), this.outbound.stop(), this.modelSelectionFlight]);
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
- sourceId: dispatch.sourceId,
1751
- workspace: dispatch.workspace,
1752
- bindingId: dispatch.bindingId,
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);