@dsh-enhanced/assistant-delivery 0.1.6 → 0.1.12

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