@dsh-enhanced/assistant-delivery 0.1.7 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +89 -10
  2. package/cordis.patch.yml +1 -0
  3. package/lib/agent-runtime.d.ts +18 -4
  4. package/lib/agent-runtime.d.ts.map +1 -1
  5. package/lib/agent-runtime.js +240 -14
  6. package/lib/agent-runtime.js.map +1 -1
  7. package/lib/canonical.d.ts +11 -1
  8. package/lib/canonical.d.ts.map +1 -1
  9. package/lib/canonical.js +107 -0
  10. package/lib/canonical.js.map +1 -1
  11. package/lib/coordinator.d.ts +6 -1
  12. package/lib/coordinator.d.ts.map +1 -1
  13. package/lib/coordinator.js +63 -24
  14. package/lib/coordinator.js.map +1 -1
  15. package/lib/feedback-command.d.ts +48 -2
  16. package/lib/feedback-command.d.ts.map +1 -1
  17. package/lib/feedback-command.js +226 -10
  18. package/lib/feedback-command.js.map +1 -1
  19. package/lib/index.d.ts +5 -3
  20. package/lib/index.d.ts.map +1 -1
  21. package/lib/index.js +5 -3
  22. package/lib/index.js.map +1 -1
  23. package/lib/learning-command.d.ts +40 -0
  24. package/lib/learning-command.d.ts.map +1 -0
  25. package/lib/learning-command.js +57 -0
  26. package/lib/learning-command.js.map +1 -0
  27. package/lib/service.d.ts +142 -10
  28. package/lib/service.d.ts.map +1 -1
  29. package/lib/service.js +1841 -62
  30. package/lib/service.js.map +1 -1
  31. package/lib/session-commands.d.ts +14 -0
  32. package/lib/session-commands.d.ts.map +1 -1
  33. package/lib/session-commands.js +30 -0
  34. package/lib/session-commands.js.map +1 -1
  35. package/lib/sqlite.d.ts +1 -1
  36. package/lib/sqlite.d.ts.map +1 -1
  37. package/lib/sqlite.js +648 -20
  38. package/lib/sqlite.js.map +1 -1
  39. package/lib/store.d.ts +375 -3
  40. package/lib/store.d.ts.map +1 -1
  41. package/lib/store.js +2956 -74
  42. package/lib/store.js.map +1 -1
  43. package/lib/types.d.ts +364 -2
  44. package/lib/types.d.ts.map +1 -1
  45. package/lib/types.js +9 -1
  46. package/lib/types.js.map +1 -1
  47. package/lib/version.d.ts +1 -1
  48. package/lib/version.d.ts.map +1 -1
  49. package/lib/version.js +1 -1
  50. package/lib/version.js.map +1 -1
  51. package/lib/workflow-auto-producer.d.ts +37 -0
  52. package/lib/workflow-auto-producer.d.ts.map +1 -0
  53. package/lib/workflow-auto-producer.js +58 -0
  54. package/lib/workflow-auto-producer.js.map +1 -0
  55. package/lib/workflow-command.d.ts +16 -0
  56. package/lib/workflow-command.d.ts.map +1 -0
  57. package/lib/workflow-command.js +52 -0
  58. package/lib/workflow-command.js.map +1 -0
  59. package/package.json +23 -3
package/lib/store.js CHANGED
@@ -1,8 +1,11 @@
1
1
  import { createHash, randomBytes, randomUUID, scryptSync, timingSafeEqual } from 'node:crypto';
2
2
  import { isAbsolute } from 'node:path';
3
- import { canonicalConversation, canonicalPrincipal, canonicalTarget } from './canonical.js';
4
- import { isExactDeliveryCommand, isPermissionDeliveryCommand, parseDeliveryCommand, permissionDispatchRecoveryCode, permissionDispatchRecoveryFromFailureCode, } from './session-commands.js';
3
+ import { ASSISTANT_GROWTH_CONTRACT_VERSION, WORKFLOW_TRACE_SOURCE_ID, growthObjectDigest, validateResolvedWorkflowAutomationTemplate, validateWorkflowAutomationTemplate, validateWorkflowAutomationTemplateContent, validateWorkflowTraceRevision, workflowAutomationTemplateContentDigest, workflowArgumentShapeDigest, workflowScopeKey, workflowTraceRevisionDigest, } from '@dsh-enhanced/assistant-growth-contract';
4
+ import { bindingMatchesOwnerRoute, canonicalBackgroundSourceId, canonicalConversation, canonicalLocalOperatorId, canonicalOwnerRouteAuthority, canonicalPrincipal, canonicalTarget, externalPrincipalId, ownerRouteAuthorityHash, ownerRouteBindingSnapshot, } from './canonical.js';
5
+ import { feedbackDispatchRecoveryCode, isFeedbackDeliveryCommand, isFeedbackDispatchRecoveryCode, isExactDeliveryCommand, isLearningDeliveryCommand, isLearningDispatchRecoveryCode, isLearningRetryableFailureCode, isPermissionDeliveryCommand, isWorkflowDeliveryCommand, isWorkflowDispatchRecoveryCode, learningDispatchRecoveryCode, parseDeliveryCommand, permissionDispatchRecoveryCode, permissionDispatchRecoveryFromFailureCode, workflowDispatchRecoveryCode, } from './session-commands.js';
6
+ import { parseFeedbackCommand } from './feedback-command.js';
5
7
  import { openDeliveryDatabase } from './sqlite.js';
8
+ import { deriveDeterministicallyDeidentifiedWorkflowTemplate, getDeterministicallyDeidentifiedWorkflowTemplate, } from './workflow-auto-producer.js';
6
9
  export class DeliveryStoreError extends Error {
7
10
  code;
8
11
  constructor(code, message) {
@@ -11,6 +14,348 @@ export class DeliveryStoreError extends Error {
11
14
  this.name = 'DeliveryStoreError';
12
15
  }
13
16
  }
17
+ function workflowTraceRevision(row) {
18
+ let parsed;
19
+ try {
20
+ parsed = JSON.parse(row.payload_json);
21
+ }
22
+ catch {
23
+ throw new DeliveryStoreError('conflict', 'workflow trace revision payload is corrupt');
24
+ }
25
+ const revision = validateWorkflowTraceRevision(parsed);
26
+ if (revision.subjectRef !== row.subject_ref || revision.version !== row.version
27
+ || revision.source.sourceId !== WORKFLOW_TRACE_SOURCE_ID
28
+ || revision.source.generation !== row.source_generation
29
+ || revision.source.authorityDigest !== row.source_authority_digest
30
+ || workflowScopeKey(revision.scope) !== row.scope_key
31
+ || revision.scope.workspace !== row.workspace || revision.scope.preset !== row.preset
32
+ || revision.disposition !== row.disposition || revision.digest !== row.digest) {
33
+ throw new DeliveryStoreError('conflict', 'workflow trace revision columns do not match payload');
34
+ }
35
+ return revision;
36
+ }
37
+ function workflowTraceOutboxEntry(row) {
38
+ return Object.freeze({
39
+ revision: workflowTraceRevision(row),
40
+ status: row.status,
41
+ attemptCount: row.attempt_count,
42
+ nextAttemptAt: row.next_attempt_at,
43
+ ...(row.failure_code === null ? {} : { failureCode: row.failure_code }),
44
+ updatedAt: row.updated_at,
45
+ });
46
+ }
47
+ function preferenceProjectionBatchKey(events) {
48
+ const identities = events.map(event => event.idempotencyKey);
49
+ return `delivery-preference-v1:${createHash('sha256')
50
+ .update('assistant-delivery-preference-projection-v1\0')
51
+ .update(JSON.stringify(identities))
52
+ .digest('hex')}`;
53
+ }
54
+ function exactPreferenceProjectionKeys(value, allowed, label) {
55
+ if (Object.keys(value).some(key => !allowed.includes(key))) {
56
+ throw new DeliveryStoreError('invalid-intent', `${label} contains unsupported fields`);
57
+ }
58
+ }
59
+ function exactPreferenceProjectionText(value, label, max) {
60
+ if (typeof value !== 'string') {
61
+ throw new DeliveryStoreError('invalid-intent', `${label} must be a string`);
62
+ }
63
+ const normalized = validateBindingText(value, label, max);
64
+ if (normalized !== value) {
65
+ throw new DeliveryStoreError('invalid-intent', `${label} is not canonical`);
66
+ }
67
+ return value;
68
+ }
69
+ function canonicalPreferenceProjectionScope(value) {
70
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
71
+ throw new DeliveryStoreError('invalid-intent', 'preference projection scope is invalid');
72
+ }
73
+ const record = value;
74
+ exactPreferenceProjectionKeys(record, ['workspace', 'preset'], 'preference projection scope');
75
+ return Object.freeze({
76
+ workspace: exactPreferenceProjectionText(record['workspace'], 'preference workspace', 4_096),
77
+ preset: exactPreferenceProjectionText(record['preset'], 'preference preset', 200),
78
+ });
79
+ }
80
+ function canonicalPreferenceCompletionIdentity(value) {
81
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
82
+ throw new DeliveryStoreError('invalid-intent', 'preference completion identity is invalid');
83
+ }
84
+ const record = value;
85
+ exactPreferenceProjectionKeys(record, [
86
+ 'bindingId', 'bindingVersion', 'sessionId', 'sourceEventId', 'sourceInboxId', 'replyOutboxId',
87
+ ], 'preference completion identity');
88
+ if (!Number.isSafeInteger(record['bindingVersion']) || record['bindingVersion'] < 1) {
89
+ throw new DeliveryStoreError('invalid-intent', 'preference binding version is invalid');
90
+ }
91
+ return Object.freeze({
92
+ bindingId: exactPreferenceProjectionText(record['bindingId'], 'preference bindingId', 500),
93
+ bindingVersion: record['bindingVersion'],
94
+ sessionId: exactPreferenceProjectionText(record['sessionId'], 'preference sessionId', 500),
95
+ sourceEventId: exactPreferenceProjectionText(record['sourceEventId'], 'preference sourceEventId', 500),
96
+ sourceInboxId: exactPreferenceProjectionText(record['sourceInboxId'], 'preference sourceInboxId', 500),
97
+ replyOutboxId: exactPreferenceProjectionText(record['replyOutboxId'], 'preference replyOutboxId', 500),
98
+ });
99
+ }
100
+ function canonicalPreferencePrincipalLineage(value) {
101
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
102
+ throw new DeliveryStoreError('invalid-intent', 'preference principal lineage is invalid');
103
+ }
104
+ const record = value;
105
+ exactPreferenceProjectionKeys(record, ['principalRecordId', 'principalVersion'], 'preference principal lineage');
106
+ if (!Number.isSafeInteger(record['principalVersion']) || record['principalVersion'] < 1) {
107
+ throw new DeliveryStoreError('invalid-intent', 'preference principal lineage version is invalid');
108
+ }
109
+ return Object.freeze({
110
+ principalRecordId: exactPreferenceProjectionText(record['principalRecordId'], 'preference principal record id', 500),
111
+ principalVersion: record['principalVersion'],
112
+ });
113
+ }
114
+ function canonicalPreferenceAdmissionCursor(value) {
115
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
116
+ throw new DeliveryStoreError('invalid-intent', 'preference admission cursor is invalid');
117
+ }
118
+ const record = value;
119
+ exactPreferenceProjectionKeys(record, ['epoch', 'sequence'], 'preference admission cursor');
120
+ if (typeof record['epoch'] !== 'string' || !/^[0-9a-f]{32}$/u.test(record['epoch'])
121
+ || !Number.isSafeInteger(record['sequence']) || record['sequence'] < 1) {
122
+ throw new DeliveryStoreError('invalid-intent', 'preference admission cursor is invalid');
123
+ }
124
+ return Object.freeze({
125
+ epoch: record['epoch'],
126
+ sequence: record['sequence'],
127
+ });
128
+ }
129
+ function canonicalPreferenceProjectionEvent(value) {
130
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
131
+ throw new DeliveryStoreError('invalid-intent', 'preference projection event is invalid');
132
+ }
133
+ const record = value;
134
+ const source = record['source'];
135
+ const scope = canonicalPreferenceProjectionScope(record['scope']);
136
+ const occurredAt = record['occurredAt'];
137
+ if (!Number.isSafeInteger(occurredAt) || occurredAt < 0) {
138
+ throw new DeliveryStoreError('invalid-intent', 'preference projection occurredAt is invalid');
139
+ }
140
+ const idempotencyKey = exactPreferenceProjectionText(record['idempotencyKey'], 'preference event idempotencyKey', 200);
141
+ if (record['actorTrust'] !== 'owner-authenticated') {
142
+ throw new DeliveryStoreError('invalid-intent', 'preference projection actor is invalid');
143
+ }
144
+ const optionalAuthorityKeys = [
145
+ ...(record['principalLineage'] === undefined ? [] : ['principalLineage']),
146
+ ...(record['admissionCursor'] === undefined ? [] : ['admissionCursor']),
147
+ ];
148
+ const admissionCursor = record['admissionCursor'] === undefined
149
+ ? undefined
150
+ : canonicalPreferenceAdmissionCursor(record['admissionCursor']);
151
+ if (source === 'delivery-completion') {
152
+ exactPreferenceProjectionKeys(record, [
153
+ 'scope', 'principalId', ...optionalAuthorityKeys, 'actorTrust', 'source',
154
+ 'occurredAt', 'idempotencyKey', 'completion',
155
+ ], 'preference completion');
156
+ return Object.freeze({
157
+ scope,
158
+ principalId: exactPreferenceProjectionText(record['principalId'], 'preference principalId', 500),
159
+ ...(record['principalLineage'] === undefined
160
+ ? {}
161
+ : { principalLineage: canonicalPreferencePrincipalLineage(record['principalLineage']) }),
162
+ ...(admissionCursor === undefined ? {} : { admissionCursor }),
163
+ actorTrust: 'owner-authenticated',
164
+ source,
165
+ occurredAt: occurredAt,
166
+ idempotencyKey,
167
+ completion: canonicalPreferenceCompletionIdentity(record['completion']),
168
+ });
169
+ }
170
+ if (source === 'delivery-observation') {
171
+ exactPreferenceProjectionKeys(record, [
172
+ 'scope', 'principalId', ...optionalAuthorityKeys, 'preferenceKey', 'candidateValue', 'stance',
173
+ 'actorTrust', 'interpretationTrust', 'source', 'occurredAt', 'idempotencyKey', 'completion',
174
+ ], 'preference observation');
175
+ if (record['preferenceKey'] !== 'response.language'
176
+ || (record['candidateValue'] !== 'zh-CN' && record['candidateValue'] !== 'en')
177
+ || record['stance'] !== 'support'
178
+ || record['interpretationTrust'] !== 'behavioral-inference') {
179
+ throw new DeliveryStoreError('invalid-intent', 'preference observation selection is invalid');
180
+ }
181
+ return Object.freeze({
182
+ scope,
183
+ principalId: exactPreferenceProjectionText(record['principalId'], 'preference principalId', 500),
184
+ ...(record['principalLineage'] === undefined
185
+ ? {}
186
+ : { principalLineage: canonicalPreferencePrincipalLineage(record['principalLineage']) }),
187
+ ...(admissionCursor === undefined ? {} : { admissionCursor }),
188
+ preferenceKey: 'response.language',
189
+ candidateValue: record['candidateValue'],
190
+ stance: 'support',
191
+ actorTrust: 'owner-authenticated',
192
+ interpretationTrust: 'behavioral-inference',
193
+ source,
194
+ occurredAt: occurredAt,
195
+ idempotencyKey,
196
+ completion: canonicalPreferenceCompletionIdentity(record['completion']),
197
+ });
198
+ }
199
+ if (source !== 'direct-owner-feedback') {
200
+ throw new DeliveryStoreError('invalid-intent', 'preference projection source is invalid');
201
+ }
202
+ exactPreferenceProjectionKeys(record, [
203
+ 'scope', 'principalId', ...optionalAuthorityKeys, 'preferenceKey', 'candidateValue', 'stance',
204
+ 'actorTrust', 'interpretationTrust', 'source', 'occurredAt', 'idempotencyKey', 'exposureTarget',
205
+ ], 'preference feedback');
206
+ if (record['stance'] !== 'support'
207
+ || (record['interpretationTrust'] !== 'explicit-selection'
208
+ && record['interpretationTrust'] !== 'typed-feedback')) {
209
+ throw new DeliveryStoreError('invalid-intent', 'preference feedback trust is invalid');
210
+ }
211
+ const key = record['preferenceKey'];
212
+ const candidate = record['candidateValue'];
213
+ const catalog = {
214
+ 'feedback.response': [
215
+ 'helpful', 'not-helpful', 'too-long', 'too-short', 'wrong-format',
216
+ 'wrong-action', 'unwanted-reminder',
217
+ ],
218
+ 'recommendation.ranking': ['recency', 'familiarity', 'evidence'],
219
+ 'response.explanation_depth': ['result-first', 'balanced', 'tutorial'],
220
+ 'response.language': ['zh-CN', 'en'],
221
+ 'response.structure': ['prose', 'bullets', 'mixed'],
222
+ 'response.verbosity': ['concise', 'balanced', 'detailed'],
223
+ 'suggestion.frequency': ['low', 'normal'],
224
+ };
225
+ if (typeof key !== 'string' || typeof candidate !== 'string'
226
+ || !catalog[key]?.includes(candidate)) {
227
+ throw new DeliveryStoreError('invalid-intent', 'preference feedback selection is invalid');
228
+ }
229
+ let exposureTarget;
230
+ if (record['exposureTarget'] !== undefined) {
231
+ if (typeof record['exposureTarget'] !== 'object' || record['exposureTarget'] === null
232
+ || Array.isArray(record['exposureTarget'])) {
233
+ throw new DeliveryStoreError('invalid-intent', 'preference exposure target is invalid');
234
+ }
235
+ const target = record['exposureTarget'];
236
+ exactPreferenceProjectionKeys(target, ['sourceInboxId', 'sourceOutboxId'], 'preference exposure target');
237
+ exposureTarget = Object.freeze({
238
+ sourceInboxId: exactPreferenceProjectionText(target['sourceInboxId'], 'preference exposure sourceInboxId', 500),
239
+ sourceOutboxId: exactPreferenceProjectionText(target['sourceOutboxId'], 'preference exposure sourceOutboxId', 500),
240
+ });
241
+ }
242
+ return Object.freeze({
243
+ scope,
244
+ principalId: exactPreferenceProjectionText(record['principalId'], 'preference principalId', 500),
245
+ ...(record['principalLineage'] === undefined
246
+ ? {}
247
+ : { principalLineage: canonicalPreferencePrincipalLineage(record['principalLineage']) }),
248
+ ...(admissionCursor === undefined ? {} : { admissionCursor }),
249
+ preferenceKey: key,
250
+ candidateValue: candidate,
251
+ stance: 'support',
252
+ actorTrust: 'owner-authenticated',
253
+ interpretationTrust: record['interpretationTrust'],
254
+ source,
255
+ occurredAt: occurredAt,
256
+ idempotencyKey,
257
+ ...(exposureTarget === undefined ? {} : { exposureTarget }),
258
+ });
259
+ }
260
+ function preferenceProjectionLane(events) {
261
+ const exact = events.map(event => event.principalLineage !== undefined
262
+ && event.admissionCursor !== undefined);
263
+ if (exact.every(value => !value))
264
+ return undefined;
265
+ if (!exact.every(Boolean)) {
266
+ throw new DeliveryStoreError('invalid-intent', 'preference projection batch mixes exact and legacy owner authority');
267
+ }
268
+ const first = events[0];
269
+ const lineage = first.principalLineage;
270
+ const cursor = first.admissionCursor;
271
+ const lane = Object.freeze({
272
+ epoch: cursor.epoch,
273
+ workspace: first.scope.workspace,
274
+ preset: first.scope.preset,
275
+ principalRecordId: lineage.principalRecordId,
276
+ principalVersion: lineage.principalVersion,
277
+ admissionSequence: cursor.sequence,
278
+ });
279
+ if (events.some(event => event.admissionCursor.epoch !== lane.epoch
280
+ || event.admissionCursor.sequence !== lane.admissionSequence
281
+ || event.scope.workspace !== lane.workspace || event.scope.preset !== lane.preset
282
+ || event.principalLineage.principalRecordId !== lane.principalRecordId
283
+ || event.principalLineage.principalVersion !== lane.principalVersion)) {
284
+ throw new DeliveryStoreError('invalid-intent', 'preference projection batch spans multiple owner admission lanes');
285
+ }
286
+ return lane;
287
+ }
288
+ function preferenceProjectionPayload(events) {
289
+ if (!Array.isArray(events) || events.length < 1 || events.length > 16) {
290
+ throw new DeliveryStoreError('invalid-intent', 'preference projection batch must contain 1-16 events');
291
+ }
292
+ const canonicalEvents = Object.freeze(events.map(event => canonicalPreferenceProjectionEvent(event)));
293
+ const lane = preferenceProjectionLane(canonicalEvents);
294
+ const identities = canonicalEvents.map(event => event.idempotencyKey);
295
+ if (new Set(identities).size !== identities.length) {
296
+ throw new DeliveryStoreError('invalid-intent', 'preference projection batch contains duplicate events');
297
+ }
298
+ const json = JSON.stringify(canonicalEvents);
299
+ if (Buffer.byteLength(json, 'utf8') > 131_072) {
300
+ throw new DeliveryStoreError('invalid-intent', 'preference projection batch exceeds its byte cap');
301
+ }
302
+ return {
303
+ batchKey: preferenceProjectionBatchKey(canonicalEvents),
304
+ digest: createHash('sha256').update(json).digest('hex'),
305
+ json,
306
+ events: canonicalEvents,
307
+ ...(lane === undefined ? {} : { lane }),
308
+ };
309
+ }
310
+ function preferenceProjectionOutboxEntry(row) {
311
+ let parsed;
312
+ try {
313
+ parsed = JSON.parse(row.events_json);
314
+ }
315
+ catch {
316
+ throw new DeliveryStoreError('conflict', 'preference projection payload is corrupt');
317
+ }
318
+ if (!Array.isArray(parsed)) {
319
+ throw new DeliveryStoreError('conflict', 'preference projection payload is not a batch');
320
+ }
321
+ const payload = preferenceProjectionPayload(parsed);
322
+ if (payload.batchKey !== row.batch_key || payload.digest !== row.payload_digest
323
+ || payload.json !== row.events_json) {
324
+ throw new DeliveryStoreError('conflict', 'preference projection columns do not match payload');
325
+ }
326
+ const storedLaneColumns = [
327
+ row.lane_epoch,
328
+ row.lane_workspace,
329
+ row.lane_preset,
330
+ row.lane_principal_record_id,
331
+ row.lane_principal_version,
332
+ row.admission_sequence,
333
+ ];
334
+ if (row.lane_kind === 'exact') {
335
+ if (storedLaneColumns.some(value => value === null) || payload.lane === undefined
336
+ || row.lane_epoch !== payload.lane.epoch
337
+ || row.lane_workspace !== payload.lane.workspace
338
+ || row.lane_preset !== payload.lane.preset
339
+ || row.lane_principal_record_id !== payload.lane.principalRecordId
340
+ || row.lane_principal_version !== payload.lane.principalVersion
341
+ || row.admission_sequence !== payload.lane.admissionSequence) {
342
+ throw new DeliveryStoreError('conflict', 'preference projection lane columns do not match payload');
343
+ }
344
+ }
345
+ else if (row.lane_kind === 'legacy'
346
+ && (payload.lane !== undefined || storedLaneColumns.some(value => value !== null))) {
347
+ throw new DeliveryStoreError('conflict', 'legacy preference projection has exact lane authority');
348
+ }
349
+ return Object.freeze({
350
+ batchKey: row.batch_key,
351
+ payloadDigest: row.payload_digest,
352
+ events: payload.events,
353
+ ...(payload.lane === undefined ? {} : { lane: payload.lane }),
354
+ attemptCount: row.attempt_count,
355
+ nextAttemptAt: row.next_attempt_at,
356
+ ...(row.failure_code === null ? {} : { failureCode: row.failure_code }),
357
+ });
358
+ }
14
359
  const imageMediaTypes = new Set(['image/png', 'image/jpeg', 'image/webp', 'image/gif']);
15
360
  function invalidImageRef() {
16
361
  throw new DeliveryStoreError('conflict', 'inbound image reference is invalid');
@@ -86,6 +431,19 @@ function attachmentFromRow(row) {
86
431
  createdAt: row.created_at,
87
432
  };
88
433
  }
434
+ function deadLetterResolutionFromRow(row) {
435
+ return {
436
+ receiptVersion: row.receipt_version,
437
+ kind: row.kind,
438
+ id: row.message_id,
439
+ attemptCount: row.attempt_count,
440
+ resolution: row.resolution,
441
+ originalStatus: row.original_status,
442
+ ...(row.original_failure_code === null ? {} : { originalFailureCode: row.original_failure_code }),
443
+ operatorId: row.operator_id,
444
+ createdAt: row.created_at,
445
+ };
446
+ }
89
447
  function digest(value) {
90
448
  return createHash('sha256').update(value).digest('hex');
91
449
  }
@@ -181,6 +539,10 @@ const bindingSelect = `
181
539
  const inboxSelect = `
182
540
  SELECT id, channel, account, event_id, envelope_hash, envelope_json, status, binding_id,
183
541
  attempt_count, next_attempt_at, claimed_by, fencing_token, lease_until, failure_code,
542
+ (SELECT epoch FROM delivery_inbox_admissions WHERE inbox_id = inbox_messages.id)
543
+ AS admission_epoch,
544
+ (SELECT admission_sequence FROM delivery_inbox_admissions WHERE inbox_id = inbox_messages.id)
545
+ AS admission_sequence,
184
546
  received_at, updated_at
185
547
  FROM inbox_messages
186
548
  `;
@@ -189,6 +551,12 @@ const outboxSelect = `
189
551
  next_attempt_at, claimed_by, fencing_token, lease_until, failure_code, created_at, updated_at
190
552
  FROM outbox_messages
191
553
  `;
554
+ const deliveryPresentationSelect = `
555
+ SELECT presentation_key, original_outbox_idempotency_key, revision, payload_hash, payload_json,
556
+ status, attempt_count, presented_revision, next_attempt_at, claimed_by, fencing_token,
557
+ lease_until, provider_message_id, failure_code, created_at, updated_at
558
+ FROM delivery_presentations
559
+ `;
192
560
  function validateBindingText(value, field, max) {
193
561
  const normalized = value.trim();
194
562
  const hasControl = [...normalized].some(character => {
@@ -200,6 +568,87 @@ function validateBindingText(value, field, max) {
200
568
  }
201
569
  return normalized;
202
570
  }
571
+ function approvalRouteText(value, field, maxBytes) {
572
+ if (typeof value !== 'string') {
573
+ throw new DeliveryStoreError('invalid-intent', `approval route ${field} is invalid`);
574
+ }
575
+ const normalized = value.normalize('NFC').trim();
576
+ const hasControl = [...normalized].some(character => {
577
+ const code = character.codePointAt(0);
578
+ return code <= 0x1f || code === 0x7f;
579
+ });
580
+ if (normalized === '' || normalized !== value || Buffer.byteLength(normalized, 'utf8') > maxBytes || hasControl) {
581
+ throw new DeliveryStoreError('invalid-intent', `approval route ${field} is invalid`);
582
+ }
583
+ return normalized;
584
+ }
585
+ function canonicalApprovalDispatchRoute(input) {
586
+ if (input === null || typeof input !== 'object' || Array.isArray(input)) {
587
+ throw new DeliveryStoreError('invalid-intent', 'approval route must be a v2 object');
588
+ }
589
+ const route = input;
590
+ const fields = [
591
+ 'bindingGeneration', 'bindingId', 'bindingVersion', 'principal', 'principalRecordId',
592
+ 'principalVersion', 'routeVersion', 'sourceId', 'workspace',
593
+ ];
594
+ if (Object.keys(route).sort().join(',') !== fields.join(',') || route.routeVersion !== 2) {
595
+ throw new DeliveryStoreError('invalid-intent', 'approval route must be complete route version 2');
596
+ }
597
+ const sourceId = approvalRouteText(route.sourceId, 'sourceId', 256);
598
+ const bindingId = approvalRouteText(route.bindingId, 'bindingId', 512);
599
+ const workspace = approvalRouteText(route.workspace, 'workspace', 4_096);
600
+ const principal = approvalRouteText(route.principal, 'principal', 512);
601
+ const principalRecordId = approvalRouteText(route.principalRecordId, 'principalRecordId', 500);
602
+ if (!isAbsolute(workspace)
603
+ || !Number.isSafeInteger(route.bindingVersion) || route.bindingVersion < 1
604
+ || !Number.isSafeInteger(route.bindingGeneration) || route.bindingGeneration < 1
605
+ || !Number.isSafeInteger(route.principalVersion) || route.principalVersion < 1) {
606
+ throw new DeliveryStoreError('invalid-intent', 'approval route fence is invalid');
607
+ }
608
+ return Object.freeze({
609
+ routeVersion: 2,
610
+ sourceId,
611
+ bindingId,
612
+ bindingVersion: route.bindingVersion,
613
+ bindingGeneration: route.bindingGeneration,
614
+ workspace,
615
+ principal,
616
+ principalRecordId,
617
+ principalVersion: route.principalVersion,
618
+ });
619
+ }
620
+ function approvalRouteFromRow(row) {
621
+ return canonicalApprovalDispatchRoute({
622
+ routeVersion: row.route_version,
623
+ sourceId: row.source_id,
624
+ bindingId: row.binding_id,
625
+ bindingVersion: row.binding_version,
626
+ bindingGeneration: row.binding_generation,
627
+ workspace: row.workspace,
628
+ principal: row.principal,
629
+ principalRecordId: row.principal_record_id,
630
+ principalVersion: row.principal_version,
631
+ });
632
+ }
633
+ function sameApprovalRoute(left, right) {
634
+ return left.routeVersion === right.routeVersion
635
+ && left.sourceId === right.sourceId
636
+ && left.bindingId === right.bindingId
637
+ && left.bindingVersion === right.bindingVersion
638
+ && left.bindingGeneration === right.bindingGeneration
639
+ && left.workspace === right.workspace
640
+ && left.principal === right.principal
641
+ && left.principalRecordId === right.principalRecordId
642
+ && left.principalVersion === right.principalVersion;
643
+ }
644
+ function resolutionOperatorId(value) {
645
+ try {
646
+ return canonicalLocalOperatorId(value);
647
+ }
648
+ catch {
649
+ throw new DeliveryStoreError('conflict', 'dead-letter operator identity is invalid');
650
+ }
651
+ }
203
652
  function boundedMaintenanceLimit(value, fallback, label) {
204
653
  const limit = value ?? fallback;
205
654
  if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) {
@@ -208,6 +657,10 @@ function boundedMaintenanceLimit(value, fallback, label) {
208
657
  return limit;
209
658
  }
210
659
  function inboxFromRow(row) {
660
+ if (!/^[0-9a-f]{32}$/u.test(row.admission_epoch)
661
+ || !Number.isSafeInteger(row.admission_sequence) || row.admission_sequence < 1) {
662
+ throw new DeliveryStoreError('conflict', 'Inbox admission cursor is missing or corrupt');
663
+ }
211
664
  return {
212
665
  id: row.id,
213
666
  channel: row.channel,
@@ -223,6 +676,10 @@ function inboxFromRow(row) {
223
676
  ...(row.fencing_token === null ? {} : { fencingToken: row.fencing_token }),
224
677
  ...(row.lease_until === null ? {} : { leaseUntil: row.lease_until }),
225
678
  ...(row.failure_code === null ? {} : { failureCode: row.failure_code }),
679
+ admissionCursor: Object.freeze({
680
+ epoch: row.admission_epoch,
681
+ sequence: row.admission_sequence,
682
+ }),
226
683
  receivedAt: row.received_at,
227
684
  updatedAt: row.updated_at,
228
685
  };
@@ -244,6 +701,137 @@ function outboxFromRow(row) {
244
701
  updatedAt: row.updated_at,
245
702
  };
246
703
  }
704
+ function deliveryPresentationFromRow(row) {
705
+ let presentation;
706
+ try {
707
+ presentation = canonicalDeliveryPresentation(JSON.parse(row.payload_json));
708
+ const canonicalJson = JSON.stringify(presentation);
709
+ if (canonicalJson !== row.payload_json || digest(canonicalJson) !== row.payload_hash
710
+ || !Number.isSafeInteger(row.revision) || row.revision < 1
711
+ || !Number.isSafeInteger(row.attempt_count) || row.attempt_count < 0
712
+ || !Number.isSafeInteger(row.presented_revision) || row.presented_revision < 0
713
+ || row.presented_revision > row.revision
714
+ || !Number.isSafeInteger(row.created_at) || !Number.isSafeInteger(row.updated_at)
715
+ || (row.status === 'presented' && row.provider_message_id === null)) {
716
+ throw new Error('invalid stored presentation');
717
+ }
718
+ }
719
+ catch {
720
+ throw new DeliveryStoreError('invalid-intent', 'stored delivery presentation is invalid');
721
+ }
722
+ return {
723
+ presentationKey: row.presentation_key,
724
+ originalOutboxIdempotencyKey: row.original_outbox_idempotency_key,
725
+ revision: row.revision,
726
+ presentation,
727
+ status: row.status,
728
+ attemptCount: row.attempt_count,
729
+ presentedRevision: row.presented_revision,
730
+ ...(row.provider_message_id === null ? {} : { providerMessageId: row.provider_message_id }),
731
+ ...(row.failure_code === null ? {} : { failureCode: row.failure_code }),
732
+ createdAt: row.created_at,
733
+ updatedAt: row.updated_at,
734
+ };
735
+ }
736
+ function canonicalDeliveryPresentation(input) {
737
+ if (input === null || typeof input !== 'object' || Array.isArray(input)) {
738
+ throw new DeliveryStoreError('invalid-intent', 'delivery presentation kind is invalid');
739
+ }
740
+ if (input.kind === 'automation-incident') {
741
+ const allowed = new Set([
742
+ 'kind', 'incidentId', 'automationId', 'definitionHash', 'stage', 'state',
743
+ 'failureClass', 'failurePhase', 'failureCode', 'sideEffectState', 'retryability',
744
+ 'lifecycleGeneration', 'incidentRevision', 'openedAt', 'updatedAt', 'resolvedAt',
745
+ ]);
746
+ if (Object.keys(input).some(key => !allowed.has(key))) {
747
+ throw new DeliveryStoreError('invalid-intent', 'automation incident presentation shape is invalid');
748
+ }
749
+ const incidentId = validateBindingText(input.incidentId, 'incidentId', 500);
750
+ const automationId = validateBindingText(input.automationId, 'automationId', 500);
751
+ const token = (value) => typeof value === 'string'
752
+ && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/u.test(value);
753
+ if (!/^[a-f0-9]{64}$/u.test(input.definitionHash)
754
+ || !['claim', 'materialize', 'terminal'].includes(input.stage)
755
+ || !['open', 'recovering', 'resolved'].includes(input.state)
756
+ || !['budget', 'cancelled', 'configuration', 'execution', 'infrastructure',
757
+ 'policy', 'provider', 'timeout', 'unknown'].includes(input.failureClass)
758
+ || !token(input.failurePhase) || !token(input.failureCode)
759
+ || !['none', 'possible', 'unknown'].includes(input.sideEffectState)
760
+ || !['after-intervention', 'safe', 'unsafe', 'unknown'].includes(input.retryability)
761
+ || !Number.isSafeInteger(input.lifecycleGeneration) || input.lifecycleGeneration < 1
762
+ || !Number.isSafeInteger(input.incidentRevision) || input.incidentRevision < 1
763
+ || !Number.isSafeInteger(input.openedAt) || input.openedAt < 0
764
+ || !Number.isSafeInteger(input.updatedAt) || input.updatedAt < input.openedAt
765
+ || (input.state === 'resolved') !== (input.resolvedAt !== undefined)
766
+ || (input.resolvedAt !== undefined
767
+ && (!Number.isSafeInteger(input.resolvedAt) || input.resolvedAt < input.openedAt))) {
768
+ throw new DeliveryStoreError('invalid-intent', 'automation incident presentation is invalid');
769
+ }
770
+ return Object.freeze({
771
+ kind: 'automation-incident',
772
+ incidentId,
773
+ automationId,
774
+ definitionHash: input.definitionHash,
775
+ stage: input.stage,
776
+ state: input.state,
777
+ failureClass: input.failureClass,
778
+ failurePhase: input.failurePhase,
779
+ failureCode: input.failureCode,
780
+ sideEffectState: input.sideEffectState,
781
+ retryability: input.retryability,
782
+ lifecycleGeneration: input.lifecycleGeneration,
783
+ incidentRevision: input.incidentRevision,
784
+ openedAt: input.openedAt,
785
+ updatedAt: input.updatedAt,
786
+ ...(input.resolvedAt === undefined ? {} : { resolvedAt: input.resolvedAt }),
787
+ });
788
+ }
789
+ if (input.kind !== 'approval-application') {
790
+ throw new DeliveryStoreError('invalid-intent', 'delivery presentation kind is invalid');
791
+ }
792
+ const allowed = new Set([
793
+ 'kind', 'policyProposalId', 'localProposalId', 'applicationStatus', 'operation',
794
+ 'terminalAt', 'receiptDigest', 'ruleId', 'resultingRuleVersion', 'ruleStatus',
795
+ ]);
796
+ if (Object.keys(input).some(key => !allowed.has(key))) {
797
+ throw new DeliveryStoreError('invalid-intent', 'delivery presentation shape is invalid');
798
+ }
799
+ const policyProposalId = validateBindingText(input.policyProposalId, 'policyProposalId', 200);
800
+ const localProposalId = validateBindingText(input.localProposalId, 'localProposalId', 200);
801
+ if (!['applied', 'conflicted', 'expired', 'rejected'].includes(input.applicationStatus)
802
+ || !['adopt', 'owner-undo', 'retire'].includes(input.operation)
803
+ || !Number.isSafeInteger(input.terminalAt) || input.terminalAt < 0
804
+ || !/^[a-f0-9]{64}$/u.test(input.receiptDigest)) {
805
+ throw new DeliveryStoreError('invalid-intent', 'approval application presentation is invalid');
806
+ }
807
+ const ruleId = input.ruleId === undefined
808
+ ? undefined
809
+ : validateBindingText(input.ruleId, 'ruleId', 200);
810
+ const resultingRuleVersion = input.resultingRuleVersion;
811
+ if (resultingRuleVersion !== undefined
812
+ && (!Number.isSafeInteger(resultingRuleVersion) || resultingRuleVersion < 1)) {
813
+ throw new DeliveryStoreError('invalid-intent', 'presentation rule version is invalid');
814
+ }
815
+ if (input.ruleStatus !== undefined && input.ruleStatus !== 'active' && input.ruleStatus !== 'retired') {
816
+ throw new DeliveryStoreError('invalid-intent', 'presentation rule status is invalid');
817
+ }
818
+ if (input.applicationStatus === 'applied'
819
+ && (ruleId === undefined || resultingRuleVersion === undefined || input.ruleStatus === undefined)) {
820
+ throw new DeliveryStoreError('invalid-intent', 'applied presentation requires exact rule terminal state');
821
+ }
822
+ return Object.freeze({
823
+ kind: 'approval-application',
824
+ policyProposalId,
825
+ localProposalId,
826
+ applicationStatus: input.applicationStatus,
827
+ operation: input.operation,
828
+ terminalAt: input.terminalAt,
829
+ receiptDigest: input.receiptDigest,
830
+ ...(ruleId === undefined ? {} : { ruleId }),
831
+ ...(resultingRuleVersion === undefined ? {} : { resultingRuleVersion }),
832
+ ...(input.ruleStatus === undefined ? {} : { ruleStatus: input.ruleStatus }),
833
+ });
834
+ }
247
835
  function invalidEnvelope(message) {
248
836
  throw new DeliveryStoreError('invalid-envelope', message);
249
837
  }
@@ -449,6 +1037,101 @@ function canonicalIntent(input, binding, maxTextBytes) {
449
1037
  ...(replyToEventId === undefined ? {} : { replyToEventId }),
450
1038
  ...(metadata === undefined ? {} : { metadata }) };
451
1039
  }
1040
+ const ownerRouteReceiptKeys = [
1041
+ 'dsh.route.authority',
1042
+ 'dsh.route.authorityHash',
1043
+ 'dsh.route.bindingVersion',
1044
+ 'dsh.route.generation',
1045
+ 'dsh.route.initialBindingId',
1046
+ 'dsh.route.initialBindingVersion',
1047
+ 'dsh.route.initialGeneration',
1048
+ 'dsh.route.minimumGeneration',
1049
+ 'dsh.route.receiptVersion',
1050
+ 'dsh.route.sourceHash',
1051
+ 'dsh.route.sourceId',
1052
+ ];
1053
+ function exactPositiveIntegerText(value) {
1054
+ if (value === undefined || !/^[1-9][0-9]*$/u.test(value))
1055
+ return undefined;
1056
+ const parsed = Number(value);
1057
+ return Number.isSafeInteger(parsed) && String(parsed) === value ? parsed : undefined;
1058
+ }
1059
+ function parseOwnerRouteReceipt(record) {
1060
+ const metadata = record.intent.metadata;
1061
+ const authorityId = metadata?.['dsh.route.authority'];
1062
+ if (authorityId === undefined)
1063
+ return { kind: 'not-route' };
1064
+ if (metadata === undefined
1065
+ || Object.keys(metadata).length !== ownerRouteReceiptKeys.length
1066
+ || Object.keys(metadata).some(key => !ownerRouteReceiptKeys.includes(key))
1067
+ || !/^[A-Za-z0-9][A-Za-z0-9._/-]{0,127}$/u.test(authorityId)
1068
+ || !/^[a-f0-9]{64}$/u.test(metadata['dsh.route.authorityHash'] ?? '')
1069
+ || !/^[a-f0-9]{64}$/u.test(metadata['dsh.route.sourceHash'] ?? '')
1070
+ || metadata['dsh.route.receiptVersion'] !== '2'
1071
+ || !['plain', 'markdown'].includes(record.intent.format ?? 'plain')
1072
+ || record.intent.approval !== undefined
1073
+ || record.intent.modelPicker !== undefined
1074
+ || record.intent.permissionPicker !== undefined
1075
+ || record.intent.replyToEventId !== undefined)
1076
+ return { kind: 'invalid' };
1077
+ let sourceId;
1078
+ let initialBindingId;
1079
+ try {
1080
+ sourceId = canonicalBackgroundSourceId(metadata['dsh.route.sourceId']);
1081
+ initialBindingId = validateBindingText(metadata['dsh.route.initialBindingId'], 'initialBindingId', 256);
1082
+ }
1083
+ catch {
1084
+ return { kind: 'invalid' };
1085
+ }
1086
+ const bindingVersion = exactPositiveIntegerText(metadata['dsh.route.bindingVersion']);
1087
+ const generation = exactPositiveIntegerText(metadata['dsh.route.generation']);
1088
+ const initialBindingVersion = exactPositiveIntegerText(metadata['dsh.route.initialBindingVersion']);
1089
+ const initialGeneration = exactPositiveIntegerText(metadata['dsh.route.initialGeneration']);
1090
+ const minimumGeneration = exactPositiveIntegerText(metadata['dsh.route.minimumGeneration']);
1091
+ const sourceHash = metadata['dsh.route.sourceHash'];
1092
+ if (bindingVersion === undefined || generation === undefined
1093
+ || initialBindingVersion === undefined || initialGeneration === undefined
1094
+ || minimumGeneration === undefined
1095
+ || sourceId !== metadata['dsh.route.sourceId']
1096
+ || digest(sourceId) !== sourceHash)
1097
+ return { kind: 'invalid' };
1098
+ return {
1099
+ kind: 'route',
1100
+ evidence: {
1101
+ authorityId,
1102
+ authorityHash: metadata['dsh.route.authorityHash'],
1103
+ bindingVersion,
1104
+ generation,
1105
+ initialBindingId,
1106
+ initialBindingVersion,
1107
+ initialGeneration,
1108
+ minimumGeneration,
1109
+ sourceHash,
1110
+ sourceId,
1111
+ },
1112
+ };
1113
+ }
1114
+ function ownerRouteReceiptMetadata(input) {
1115
+ const snapshot = ownerRouteBindingSnapshot(input.authority, input.binding);
1116
+ const initial = input.initial ?? {
1117
+ initialBindingId: snapshot.bindingId,
1118
+ initialBindingVersion: snapshot.bindingVersion,
1119
+ initialGeneration: snapshot.generation,
1120
+ };
1121
+ return {
1122
+ 'dsh.route.authority': snapshot.authorityId,
1123
+ 'dsh.route.authorityHash': snapshot.authorityHash,
1124
+ 'dsh.route.bindingVersion': String(snapshot.bindingVersion),
1125
+ 'dsh.route.generation': String(snapshot.generation),
1126
+ 'dsh.route.initialBindingId': initial.initialBindingId,
1127
+ 'dsh.route.initialBindingVersion': String(initial.initialBindingVersion),
1128
+ 'dsh.route.initialGeneration': String(initial.initialGeneration),
1129
+ 'dsh.route.minimumGeneration': String(snapshot.minimumGeneration),
1130
+ 'dsh.route.receiptVersion': String(snapshot.receiptVersion),
1131
+ 'dsh.route.sourceHash': digest(input.sourceId),
1132
+ 'dsh.route.sourceId': input.sourceId,
1133
+ };
1134
+ }
452
1135
  function canonicalPermissionPicker(input, binding) {
453
1136
  if (input === undefined || input === null || typeof input !== 'object' || Array.isArray(input)
454
1137
  || Object.keys(input).some(field => ![
@@ -601,6 +1284,29 @@ export class DeliveryStore {
601
1284
  throw new DeliveryStoreError('conflict', 'delivery database instance namespace is missing or invalid');
602
1285
  }
603
1286
  this.databaseInstanceId = instance.instance_id;
1287
+ const now = this.now();
1288
+ const authorityDigest = growthObjectDigest({
1289
+ contract: 'assistant-delivery-workflow-trace-source/v1',
1290
+ databaseInstanceId: this.databaseInstanceId,
1291
+ sourceId: WORKFLOW_TRACE_SOURCE_ID,
1292
+ });
1293
+ this.database.prepare(`
1294
+ INSERT INTO workflow_trace_source(
1295
+ singleton, contract_version, generation, authority_digest, created_at, updated_at
1296
+ ) VALUES (1, 1, 1, ?, ?, ?)
1297
+ ON CONFLICT(singleton) DO NOTHING
1298
+ `).run(authorityDigest, now, now);
1299
+ const traceSource = this.database.prepare(`
1300
+ SELECT contract_version, generation, authority_digest
1301
+ FROM workflow_trace_source WHERE singleton = 1
1302
+ `).get();
1303
+ if (traceSource?.contract_version !== ASSISTANT_GROWTH_CONTRACT_VERSION
1304
+ || traceSource.generation < 1 || !/^[a-f0-9]{64}$/u.test(traceSource.authority_digest)
1305
+ || traceSource.authority_digest !== authorityDigest) {
1306
+ this.database.close();
1307
+ this.closed = true;
1308
+ throw new DeliveryStoreError('conflict', 'workflow trace source authority is missing or stale');
1309
+ }
604
1310
  }
605
1311
  instanceId() {
606
1312
  this.assertOpen();
@@ -838,6 +1544,23 @@ export class DeliveryStore {
838
1544
  SELECT id, principal_json, role, status, linked_to_id, created_at, updated_at, version
839
1545
  FROM delivery_principals WHERE id = ?
840
1546
  `).get(replacement.id);
1547
+ // Projection authority is the exact durable owner row, not the external
1548
+ // principal string. In particular, A -> B -> A gives A a new row
1549
+ // version. Retire every batch that cannot belong to this exact version
1550
+ // in the same transaction as the handoff so restart/requeue can never
1551
+ // resurrect an older owner's evidence. Legacy and unclassified rows are
1552
+ // deliberately included because they cannot prove an exact lineage.
1553
+ this.database.prepare(`
1554
+ UPDATE delivery_preference_projection_outbox
1555
+ SET terminal_at = ?, failure_code = 'owner-lineage-retired',
1556
+ next_attempt_at = 9007199254740991, updated_at = ?
1557
+ WHERE terminal_at IS NULL AND status IN ('pending', 'retry_wait')
1558
+ AND NOT (
1559
+ lane_kind = 'exact'
1560
+ AND lane_principal_record_id = ?
1561
+ AND lane_principal_version = ?
1562
+ )
1563
+ `).run(now, now, result.id, result.version);
841
1564
  return principalFromRow(result);
842
1565
  });
843
1566
  }
@@ -855,6 +1578,16 @@ export class DeliveryStore {
855
1578
  `).get(principalHash(input));
856
1579
  return row === undefined ? undefined : principalFromRow(row);
857
1580
  }
1581
+ getBindingPrincipal(bindingId) {
1582
+ const row = this.database.prepare(`
1583
+ SELECT principal.id, principal.principal_json, principal.role, principal.status,
1584
+ principal.linked_to_id, principal.created_at, principal.updated_at, principal.version
1585
+ FROM conversation_bindings AS binding
1586
+ JOIN delivery_principals AS principal ON principal.id = binding.principal_id
1587
+ WHERE binding.id = ?
1588
+ `).get(bindingId);
1589
+ return row === undefined ? undefined : principalFromRow(row);
1590
+ }
858
1591
  createBinding(input) {
859
1592
  this.assertOpen();
860
1593
  const target = canonicalTarget({ conversation: input.conversation, principal: input.principal });
@@ -942,11 +1675,133 @@ export class DeliveryStore {
942
1675
  return this.database.prepare(`${bindingSelect} WHERE status = 'active' ORDER BY created_at, id`)
943
1676
  .all().map(bindingFromRow);
944
1677
  }
1678
+ /** Snapshot one exact active owner binding under the Delivery write fence. */
1679
+ resolveApprovalRouteByBinding(input) {
1680
+ this.assertOpen();
1681
+ let sourceId;
1682
+ let bindingId;
1683
+ let workspace;
1684
+ let preset;
1685
+ let principalId;
1686
+ try {
1687
+ sourceId = canonicalBackgroundSourceId(input.sourceId);
1688
+ bindingId = approvalRouteText(input.bindingId, 'bindingId', 512);
1689
+ workspace = approvalRouteText(input.scope.workspace, 'workspace', 4_096);
1690
+ preset = approvalRouteText(input.scope.preset, 'preset', 256);
1691
+ principalId = input.principalId === undefined
1692
+ ? undefined
1693
+ : approvalRouteText(input.principalId, 'principalId', 512);
1694
+ }
1695
+ catch {
1696
+ throw new DeliveryStoreError('invalid-binding', 'approval route resolution input is invalid');
1697
+ }
1698
+ if (!isAbsolute(workspace)) {
1699
+ throw new DeliveryStoreError('invalid-binding', 'approval route workspace is invalid');
1700
+ }
1701
+ return this.transaction(() => {
1702
+ const binding = this.getBinding(bindingId);
1703
+ const owner = binding === undefined ? undefined : this.getBindingPrincipal(binding.id);
1704
+ const currentPrincipalId = binding === undefined ? undefined : externalPrincipalId(binding.principal);
1705
+ if (binding?.status !== 'active' || binding.workspace !== workspace || binding.agentPreset !== preset
1706
+ || owner?.status !== 'active' || owner.role !== 'owner'
1707
+ || JSON.stringify(owner.principal) !== JSON.stringify(binding.principal)
1708
+ || currentPrincipalId === undefined
1709
+ || (principalId !== undefined && currentPrincipalId !== principalId))
1710
+ return undefined;
1711
+ return Object.freeze({
1712
+ routeVersion: 2,
1713
+ sourceId,
1714
+ bindingId: binding.id,
1715
+ bindingVersion: binding.version,
1716
+ bindingGeneration: binding.generation,
1717
+ workspace: binding.workspace,
1718
+ principal: currentPrincipalId,
1719
+ principalRecordId: owner.id,
1720
+ principalVersion: owner.version,
1721
+ });
1722
+ });
1723
+ }
1724
+ /** Resolve one unique current owner from content-free Host authority evidence. */
1725
+ resolveOwnerApprovalForPreference(input) {
1726
+ this.assertOpen();
1727
+ let sourceId;
1728
+ let workspace;
1729
+ let preset;
1730
+ let principalId;
1731
+ let principalRecordId;
1732
+ try {
1733
+ sourceId = canonicalBackgroundSourceId(input.sourceId);
1734
+ workspace = approvalRouteText(input.scope.workspace, 'workspace', 4_096);
1735
+ preset = approvalRouteText(input.scope.preset, 'preset', 256);
1736
+ principalId = approvalRouteText(input.principalId, 'principalId', 512);
1737
+ principalRecordId = approvalRouteText(input.principalLineage.principalRecordId, 'principalRecordId', 500);
1738
+ }
1739
+ catch {
1740
+ throw new DeliveryStoreError('invalid-binding', 'owner approval authority input is invalid');
1741
+ }
1742
+ if (!isAbsolute(workspace)
1743
+ || !Number.isSafeInteger(input.principalLineage.principalVersion)
1744
+ || input.principalLineage.principalVersion < 1
1745
+ || !Number.isSafeInteger(input.ownerGeneration) || input.ownerGeneration < 1) {
1746
+ throw new DeliveryStoreError('invalid-binding', 'owner approval authority fence is invalid');
1747
+ }
1748
+ return this.transaction(() => {
1749
+ const rows = this.database.prepare(`
1750
+ SELECT binding.id, binding.conversation_json, binding.principal_json, binding.workspace,
1751
+ binding.agent_preset, binding.session_id, binding.generation, binding.policy_ref,
1752
+ binding.status, binding.created_at, binding.updated_at, binding.version
1753
+ FROM conversation_bindings AS binding
1754
+ JOIN delivery_principals AS principal ON principal.id = binding.principal_id
1755
+ WHERE binding.status = 'active' AND binding.workspace = ? AND binding.agent_preset = ?
1756
+ AND principal.id = ? AND principal.version = ?
1757
+ AND principal.status = 'active' AND principal.role = 'owner'
1758
+ ORDER BY binding.id LIMIT 2
1759
+ `).all(workspace, preset, principalRecordId, input.principalLineage.principalVersion);
1760
+ if (rows.length !== 1)
1761
+ return undefined;
1762
+ const binding = bindingFromRow(rows[0]);
1763
+ const owner = this.getBindingPrincipal(binding.id);
1764
+ if (externalPrincipalId(binding.principal) !== principalId
1765
+ || binding.generation !== input.ownerGeneration
1766
+ || owner?.status !== 'active' || owner.role !== 'owner'
1767
+ || owner.id !== principalRecordId || owner.version !== input.principalLineage.principalVersion
1768
+ || JSON.stringify(owner.principal) !== JSON.stringify(binding.principal))
1769
+ return undefined;
1770
+ return Object.freeze({
1771
+ routeVersion: 2,
1772
+ sourceId,
1773
+ bindingId: binding.id,
1774
+ bindingVersion: binding.version,
1775
+ bindingGeneration: binding.generation,
1776
+ workspace: binding.workspace,
1777
+ principal: principalId,
1778
+ principalRecordId,
1779
+ principalVersion: input.principalLineage.principalVersion,
1780
+ });
1781
+ });
1782
+ }
945
1783
  getBindingBySession(sessionId) {
946
1784
  this.assertOpen();
947
1785
  const row = this.database.prepare(`${bindingSelect} WHERE session_id = ?`).get(sessionId);
948
1786
  return row === undefined ? undefined : bindingFromRow(row);
949
1787
  }
1788
+ /** Read one exact Agent owner attestation under a single SQLite snapshot. */
1789
+ getPreferencePrincipalForSession(input) {
1790
+ this.assertOpen();
1791
+ if (typeof input.sessionId !== 'string' || typeof input.workspace !== 'string'
1792
+ || typeof input.preset !== 'string')
1793
+ return undefined;
1794
+ return this.transaction(() => {
1795
+ const binding = this.getBindingBySession(input.sessionId);
1796
+ const principal = binding === undefined ? undefined : this.getBindingPrincipal(binding.id);
1797
+ if (binding?.status !== 'active' || binding.sessionId !== input.sessionId
1798
+ || binding.workspace !== input.workspace || binding.agentPreset !== input.preset
1799
+ || principal?.status !== 'active' || principal.role !== 'owner'
1800
+ || JSON.stringify(principal.principal) !== JSON.stringify(binding.principal))
1801
+ return undefined;
1802
+ return Object.freeze({ binding, principal });
1803
+ });
1804
+ }
950
1805
  getModelSelection(input) {
951
1806
  this.assertOpen();
952
1807
  const conversation = canonicalConversation(input);
@@ -1362,20 +2217,26 @@ export class DeliveryStore {
1362
2217
  const bindingPrincipalJson = principalJson(binding.principal);
1363
2218
  const now = this.now();
1364
2219
  return this.transaction(() => {
1365
- const boundaryRow = this.database.prepare('SELECT rowid FROM inbox_messages WHERE id = ?')
1366
- .get(beforeInboxId);
2220
+ const boundaryRow = this.database.prepare(`
2221
+ SELECT admission.admission_sequence
2222
+ FROM inbox_messages AS message
2223
+ JOIN delivery_inbox_admissions AS admission ON admission.inbox_id = message.id
2224
+ WHERE message.id = ?
2225
+ `).get(beforeInboxId);
1367
2226
  if (boundaryRow === undefined) {
1368
2227
  throw new DeliveryStoreError('not-found', 'inbox cancellation boundary was not found');
1369
2228
  }
1370
2229
  const dispatchingRows = this.database.prepare(`
1371
- SELECT id, envelope_json FROM inbox_messages
1372
- WHERE rowid < ? AND status = 'claimed' AND failure_code = 'dispatch-started'
2230
+ SELECT message.id, message.envelope_json FROM inbox_messages AS message
2231
+ JOIN delivery_inbox_admissions AS admission ON admission.inbox_id = message.id
2232
+ WHERE admission.admission_sequence < ?
2233
+ AND message.status = 'claimed' AND message.failure_code = 'dispatch-started'
1373
2234
  AND (binding_id = ? OR (
1374
2235
  json_extract(envelope_json, '$.conversation') = json(?)
1375
2236
  AND json_extract(envelope_json, '$.principal') = json(?)
1376
2237
  ))
1377
- ORDER BY rowid
1378
- `).all(boundaryRow.rowid, bindingId, bindingConversationJson, bindingPrincipalJson);
2238
+ ORDER BY admission.admission_sequence
2239
+ `).all(boundaryRow.admission_sequence, bindingId, bindingConversationJson, bindingPrincipalJson);
1379
2240
  for (const row of dispatchingRows) {
1380
2241
  let permission = false;
1381
2242
  try {
@@ -1393,36 +2254,44 @@ export class DeliveryStore {
1393
2254
  }
1394
2255
  }
1395
2256
  const claimedRows = this.database.prepare(`
1396
- SELECT id FROM inbox_messages
1397
- WHERE rowid < ? AND status = 'claimed' AND failure_code IS NULL
2257
+ SELECT message.id FROM inbox_messages AS message
2258
+ JOIN delivery_inbox_admissions AS admission ON admission.inbox_id = message.id
2259
+ WHERE admission.admission_sequence < ?
2260
+ AND message.status = 'claimed' AND message.failure_code IS NULL
1398
2261
  AND (binding_id = ? OR (
1399
2262
  json_extract(envelope_json, '$.conversation') = json(?)
1400
2263
  AND json_extract(envelope_json, '$.principal') = json(?)
1401
2264
  ))
1402
- ORDER BY rowid
1403
- `).all(boundaryRow.rowid, bindingId, bindingConversationJson, bindingPrincipalJson);
2265
+ ORDER BY admission.admission_sequence
2266
+ `).all(boundaryRow.admission_sequence, bindingId, bindingConversationJson, bindingPrincipalJson);
1404
2267
  this.database.prepare(`
1405
2268
  UPDATE inbox_attempts SET status = 'dead_letter', failure_code = ?, finished_at = ?
1406
2269
  WHERE status = 'claimed' AND inbox_id IN (
1407
- SELECT id FROM inbox_messages
1408
- WHERE rowid < ? AND status = 'claimed' AND failure_code IS NULL
2270
+ SELECT message.id FROM inbox_messages AS message
2271
+ JOIN delivery_inbox_admissions AS admission ON admission.inbox_id = message.id
2272
+ WHERE admission.admission_sequence < ?
2273
+ AND message.status = 'claimed' AND message.failure_code IS NULL
1409
2274
  AND (binding_id = ? OR (
1410
2275
  json_extract(envelope_json, '$.conversation') = json(?)
1411
2276
  AND json_extract(envelope_json, '$.principal') = json(?)
1412
2277
  ))
1413
2278
  )
1414
- `).run(failureCode, now, boundaryRow.rowid, bindingId, bindingConversationJson, bindingPrincipalJson);
2279
+ `).run(failureCode, now, boundaryRow.admission_sequence, bindingId, bindingConversationJson, bindingPrincipalJson);
1415
2280
  const cancelled = this.database.prepare(`
1416
2281
  UPDATE inbox_messages SET status = 'dead_letter', failure_code = ?, next_attempt_at = NULL,
1417
2282
  claimed_by = NULL, fencing_token = NULL, lease_until = NULL, updated_at = ?
1418
- WHERE rowid < ?
1419
- AND (binding_id = ? OR (
1420
- json_extract(envelope_json, '$.conversation') = json(?)
1421
- AND json_extract(envelope_json, '$.principal') = json(?)
1422
- ))
1423
- AND (status IN ('received', 'authorized', 'queued', 'retry_wait')
1424
- OR (status = 'claimed' AND failure_code IS NULL))
1425
- `).run(failureCode, now, boundaryRow.rowid, bindingId, bindingConversationJson, bindingPrincipalJson);
2283
+ WHERE id IN (
2284
+ SELECT message.id FROM inbox_messages AS message
2285
+ JOIN delivery_inbox_admissions AS admission ON admission.inbox_id = message.id
2286
+ WHERE admission.admission_sequence < ?
2287
+ AND (message.binding_id = ? OR (
2288
+ json_extract(message.envelope_json, '$.conversation') = json(?)
2289
+ AND json_extract(message.envelope_json, '$.principal') = json(?)
2290
+ ))
2291
+ AND (message.status IN ('received', 'authorized', 'queued', 'retry_wait')
2292
+ OR (message.status = 'claimed' AND message.failure_code IS NULL))
2293
+ )
2294
+ `).run(failureCode, now, boundaryRow.admission_sequence, bindingId, bindingConversationJson, bindingPrincipalJson);
1426
2295
  const cancelledCount = Number(cancelled.changes);
1427
2296
  if (!Number.isSafeInteger(cancelledCount)) {
1428
2297
  throw new DeliveryStoreError('conflict', 'inbox cancellation count is outside the safe integer range');
@@ -1451,36 +2320,106 @@ export class DeliveryStore {
1451
2320
  this.assertOpen();
1452
2321
  const target = canonicalTarget({ conversation: input.conversation, principal: input.principal });
1453
2322
  const beforeInboxId = validateBindingText(input.beforeInboxId, 'beforeInboxId', 256);
1454
- const boundary = this.database.prepare('SELECT rowid FROM inbox_messages WHERE id = ?')
1455
- .get(beforeInboxId);
2323
+ const boundary = this.database.prepare(`
2324
+ SELECT admission.admission_sequence
2325
+ FROM inbox_messages AS message
2326
+ JOIN delivery_inbox_admissions AS admission ON admission.inbox_id = message.id
2327
+ WHERE message.id = ?
2328
+ `).get(beforeInboxId);
1456
2329
  if (boundary === undefined)
1457
2330
  throw new DeliveryStoreError('not-found', 'inbound recovery boundary Inbox was not found');
1458
2331
  const row = this.database.prepare(`${inboxSelect}
1459
- WHERE rowid < ? AND status IN ('received', 'authorized')
2332
+ JOIN delivery_inbox_admissions AS pending_admission ON pending_admission.inbox_id = inbox_messages.id
2333
+ WHERE pending_admission.admission_sequence < ? AND status IN ('received', 'authorized')
1460
2334
  AND json_extract(envelope_json, '$.conversation') = json(?)
1461
2335
  AND json_extract(envelope_json, '$.principal') = json(?)
1462
- ORDER BY rowid LIMIT 1
1463
- `).get(boundary.rowid, conversationJson(target.conversation), principalJson(target.principal));
2336
+ ORDER BY pending_admission.admission_sequence LIMIT 1
2337
+ `).get(boundary.admission_sequence, conversationJson(target.conversation), principalJson(target.principal));
1464
2338
  return row === undefined ? undefined : inboxFromRow(row);
1465
2339
  }
1466
2340
  listInbox(input = {}) {
1467
2341
  this.assertOpen();
1468
2342
  const limit = Math.max(1, Math.min(100, input.limit ?? 20));
1469
2343
  const rows = input.bindingId === undefined
1470
- ? this.database.prepare(`${inboxSelect} ORDER BY received_at DESC, rowid DESC LIMIT ?`).all(limit)
1471
- : this.database.prepare(`${inboxSelect} WHERE binding_id = ? ORDER BY received_at DESC, rowid DESC LIMIT ?`)
2344
+ ? this.database.prepare(`${inboxSelect} ORDER BY received_at DESC, admission_sequence DESC LIMIT ?`).all(limit)
2345
+ : this.database.prepare(`${inboxSelect} WHERE binding_id = ? ORDER BY received_at DESC, admission_sequence DESC LIMIT ?`)
1472
2346
  .all(input.bindingId, limit);
1473
2347
  return rows.map(inboxFromRow);
1474
2348
  }
2349
+ getDeadLetterResolution(input) {
2350
+ this.assertOpen();
2351
+ if (!['inbox', 'outbox'].includes(input.kind)
2352
+ || !Number.isSafeInteger(input.attemptCount) || input.attemptCount < 0) {
2353
+ throw new DeliveryStoreError('conflict', 'dead-letter resolution identity is invalid');
2354
+ }
2355
+ const id = validateBindingText(input.id, 'messageId', 256);
2356
+ const row = this.database.prepare(`
2357
+ SELECT kind, message_id, attempt_count, receipt_version, resolution, original_status,
2358
+ original_failure_code, operator_id, created_at
2359
+ FROM dead_letter_resolutions
2360
+ WHERE kind = ? AND message_id = ? AND attempt_count = ?
2361
+ `).get(input.kind, id, input.attemptCount);
2362
+ return row === undefined ? undefined : deadLetterResolutionFromRow(row);
2363
+ }
1475
2364
  health() {
1476
2365
  this.assertOpen();
1477
2366
  const scalar = (sql) => this.database.prepare(sql).get().count;
1478
2367
  return {
1479
2368
  pendingInbox: scalar("SELECT COUNT(*) AS count FROM inbox_messages WHERE status IN ('received', 'authorized', 'queued', 'claimed', 'retry_wait')"),
1480
2369
  deadLetterInbox: scalar("SELECT COUNT(*) AS count FROM inbox_messages WHERE status = 'dead_letter'"),
2370
+ actionableDeadLetterInbox: scalar(`
2371
+ SELECT COUNT(*) AS count FROM inbox_messages AS message
2372
+ WHERE message.status = 'dead_letter' AND NOT EXISTS (
2373
+ SELECT 1 FROM dead_letter_resolutions AS resolution
2374
+ WHERE resolution.kind = 'inbox' AND resolution.message_id = message.id
2375
+ AND resolution.attempt_count = message.attempt_count AND resolution.resolution = 'cancel'
2376
+ )
2377
+ `),
2378
+ resolvedDeadLetterInbox: scalar(`
2379
+ SELECT COUNT(*) AS count FROM inbox_messages AS message
2380
+ WHERE message.status = 'dead_letter' AND EXISTS (
2381
+ SELECT 1 FROM dead_letter_resolutions AS resolution
2382
+ WHERE resolution.kind = 'inbox' AND resolution.message_id = message.id
2383
+ AND resolution.attempt_count = message.attempt_count AND resolution.resolution = 'cancel'
2384
+ )
2385
+ `),
1481
2386
  pendingOutbox: scalar("SELECT COUNT(*) AS count FROM outbox_messages WHERE status IN ('pending', 'attempting', 'retry_wait')"),
1482
2387
  deadLetterOutbox: scalar("SELECT COUNT(*) AS count FROM outbox_messages WHERE status = 'dead'"),
2388
+ actionableDeadLetterOutbox: scalar(`
2389
+ SELECT COUNT(*) AS count FROM outbox_messages AS message
2390
+ WHERE message.status = 'dead' AND NOT EXISTS (
2391
+ SELECT 1 FROM dead_letter_resolutions AS resolution
2392
+ WHERE resolution.kind = 'outbox' AND resolution.message_id = message.id
2393
+ AND resolution.attempt_count = message.attempt_count AND resolution.resolution = 'cancel'
2394
+ )
2395
+ `),
2396
+ resolvedDeadLetterOutbox: scalar(`
2397
+ SELECT COUNT(*) AS count FROM outbox_messages AS message
2398
+ WHERE message.status = 'dead' AND EXISTS (
2399
+ SELECT 1 FROM dead_letter_resolutions AS resolution
2400
+ WHERE resolution.kind = 'outbox' AND resolution.message_id = message.id
2401
+ AND resolution.attempt_count = message.attempt_count AND resolution.resolution = 'cancel'
2402
+ )
2403
+ `),
1483
2404
  unknownOutbox: scalar("SELECT COUNT(*) AS count FROM outbox_messages WHERE status = 'unknown_after_send'"),
2405
+ actionableUnknownOutbox: scalar(`
2406
+ SELECT COUNT(*) AS count FROM outbox_messages AS message
2407
+ WHERE message.status = 'unknown_after_send' AND NOT EXISTS (
2408
+ SELECT 1 FROM dead_letter_resolutions AS resolution
2409
+ WHERE resolution.kind = 'outbox' AND resolution.message_id = message.id
2410
+ AND resolution.attempt_count = message.attempt_count AND resolution.resolution = 'cancel'
2411
+ )
2412
+ `),
2413
+ resolvedUnknownOutbox: scalar(`
2414
+ SELECT COUNT(*) AS count FROM outbox_messages AS message
2415
+ WHERE message.status = 'unknown_after_send' AND EXISTS (
2416
+ SELECT 1 FROM dead_letter_resolutions AS resolution
2417
+ WHERE resolution.kind = 'outbox' AND resolution.message_id = message.id
2418
+ AND resolution.attempt_count = message.attempt_count AND resolution.resolution = 'cancel'
2419
+ )
2420
+ `),
2421
+ pendingPresentations: scalar("SELECT COUNT(*) AS count FROM delivery_presentations WHERE status IN ('pending', 'attempting', 'retry_wait')"),
2422
+ deadPresentations: scalar("SELECT COUNT(*) AS count FROM delivery_presentations WHERE status = 'dead'"),
1484
2423
  };
1485
2424
  }
1486
2425
  getApprovalDispatchCursor() {
@@ -1571,38 +2510,62 @@ export class DeliveryStore {
1571
2510
  this.database.prepare(`
1572
2511
  UPDATE inbox_messages SET status = 'dead_letter', failure_code = 'attempts-exhausted',
1573
2512
  next_attempt_at = NULL, updated_at = ?
1574
- WHERE rowid IN (
1575
- SELECT rowid FROM inbox_messages
1576
- WHERE status = 'retry_wait' AND next_attempt_at <= ? AND attempt_count >= ?
1577
- AND (failure_code IS NULL OR failure_code NOT IN (
2513
+ WHERE id IN (
2514
+ SELECT candidate.id FROM inbox_messages AS candidate
2515
+ JOIN delivery_inbox_admissions AS candidate_admission
2516
+ ON candidate_admission.inbox_id = candidate.id
2517
+ WHERE candidate.status = 'retry_wait' AND candidate.next_attempt_at <= ?
2518
+ AND candidate.attempt_count >= ?
2519
+ AND (candidate.failure_code IS NULL OR candidate.failure_code NOT IN (
1578
2520
  'permission-dispatch-recovery',
1579
2521
  'permission-cancelled-recovery',
1580
- 'permission-failure-notice-recovery'
2522
+ 'permission-failure-notice-recovery',
2523
+ 'feedback-dispatch-recovery',
2524
+ 'learning-dispatch-recovery',
2525
+ 'workflow-dispatch-recovery'
1581
2526
  ))
1582
- ORDER BY rowid LIMIT ?
2527
+ ORDER BY candidate_admission.admission_sequence LIMIT ?
1583
2528
  )
1584
2529
  `).run(now, now, input.maxAttempts, maintenanceLimit);
1585
2530
  const candidates = this.database.prepare(`
1586
2531
  SELECT candidate.id FROM inbox_messages AS candidate
2532
+ JOIN conversation_bindings AS candidate_binding
2533
+ ON candidate_binding.id = candidate.binding_id
2534
+ JOIN delivery_inbox_admissions AS candidate_admission
2535
+ ON candidate_admission.inbox_id = candidate.id
1587
2536
  WHERE candidate.status IN ('queued', 'retry_wait')
1588
2537
  AND (candidate.next_attempt_at IS NULL OR candidate.next_attempt_at <= ?)
1589
2538
  AND (candidate.status = 'queued' OR candidate.attempt_count < ? OR candidate.failure_code IN (
1590
2539
  'permission-dispatch-recovery',
1591
2540
  'permission-cancelled-recovery',
1592
- 'permission-failure-notice-recovery'
2541
+ 'permission-failure-notice-recovery',
2542
+ 'feedback-dispatch-recovery',
2543
+ 'learning-dispatch-recovery',
2544
+ 'workflow-dispatch-recovery'
1593
2545
  ))
1594
2546
  AND candidate.binding_id IS NOT NULL
1595
2547
  AND NOT EXISTS (
1596
2548
  SELECT 1 FROM inbox_messages AS earlier
1597
- WHERE earlier.binding_id = candidate.binding_id
1598
- AND earlier.rowid < candidate.rowid
2549
+ JOIN conversation_bindings AS earlier_binding
2550
+ ON earlier_binding.id = earlier.binding_id
2551
+ JOIN delivery_inbox_admissions AS earlier_admission
2552
+ ON earlier_admission.inbox_id = earlier.id
2553
+ WHERE earlier_binding.workspace = candidate_binding.workspace
2554
+ AND earlier_binding.agent_preset = candidate_binding.agent_preset
2555
+ AND earlier_binding.principal_id = candidate_binding.principal_id
2556
+ AND earlier_admission.admission_sequence < candidate_admission.admission_sequence
1599
2557
  AND earlier.status NOT IN ('processed', 'dead_letter')
1600
2558
  )
1601
2559
  AND NOT EXISTS (
1602
2560
  SELECT 1 FROM inbox_messages AS active
1603
- WHERE active.binding_id = candidate.binding_id AND active.status = 'claimed'
2561
+ JOIN conversation_bindings AS active_binding
2562
+ ON active_binding.id = active.binding_id
2563
+ WHERE active_binding.workspace = candidate_binding.workspace
2564
+ AND active_binding.agent_preset = candidate_binding.agent_preset
2565
+ AND active_binding.principal_id = candidate_binding.principal_id
2566
+ AND active.status = 'claimed'
1604
2567
  )
1605
- ORDER BY candidate.rowid LIMIT ?
2568
+ ORDER BY candidate_admission.admission_sequence LIMIT ?
1606
2569
  `).all(now, input.maxAttempts, input.limit);
1607
2570
  for (const candidate of candidates) {
1608
2571
  const current = this.getInbox(candidate.id);
@@ -1613,7 +2576,10 @@ export class DeliveryStore {
1613
2576
  failure_code = CASE WHEN failure_code IN (
1614
2577
  'permission-dispatch-recovery',
1615
2578
  'permission-cancelled-recovery',
1616
- 'permission-failure-notice-recovery'
2579
+ 'permission-failure-notice-recovery',
2580
+ 'feedback-dispatch-recovery',
2581
+ 'learning-dispatch-recovery',
2582
+ 'workflow-dispatch-recovery'
1617
2583
  ) THEN failure_code ELSE NULL END,
1618
2584
  updated_at = ?
1619
2585
  WHERE id = ? AND status IN ('queued', 'retry_wait')
@@ -1675,7 +2641,10 @@ export class DeliveryStore {
1675
2641
  UPDATE inbox_messages AS inbox SET failure_code = CASE WHEN failure_code IN (
1676
2642
  'permission-dispatch-recovery',
1677
2643
  'permission-cancelled-recovery',
1678
- 'permission-failure-notice-recovery'
2644
+ 'permission-failure-notice-recovery',
2645
+ 'feedback-dispatch-recovery',
2646
+ 'learning-dispatch-recovery',
2647
+ 'workflow-dispatch-recovery'
1679
2648
  ) THEN failure_code ELSE 'dispatch-started' END, updated_at = ?
1680
2649
  WHERE inbox.id = ? AND inbox.status = 'claimed' AND inbox.claimed_by = ?
1681
2650
  AND inbox.fencing_token = ? AND inbox.lease_until > ? AND inbox.binding_id = ?
@@ -1726,18 +2695,34 @@ export class DeliveryStore {
1726
2695
  const recovered = [];
1727
2696
  this.transaction(() => {
1728
2697
  const rows = this.database.prepare(`
1729
- SELECT id, event_id, binding_id, attempt_count, failure_code, envelope_json FROM inbox_messages
1730
- WHERE status = 'claimed' AND lease_until <= ? ORDER BY rowid LIMIT ?
2698
+ SELECT message.id, message.event_id, message.binding_id, message.attempt_count,
2699
+ message.failure_code, message.envelope_json
2700
+ FROM inbox_messages AS message
2701
+ JOIN delivery_inbox_admissions AS admission ON admission.inbox_id = message.id
2702
+ WHERE message.status = 'claimed' AND message.lease_until <= ?
2703
+ ORDER BY admission.admission_sequence LIMIT ?
1731
2704
  `).all(now, limit);
1732
2705
  for (const row of rows) {
1733
2706
  let interruptedPermission = false;
2707
+ let interruptedFeedback = false;
2708
+ let interruptedLearning = false;
2709
+ let interruptedWorkflow = false;
1734
2710
  if (row.failure_code === 'dispatch-started') {
1735
2711
  try {
1736
- interruptedPermission = isPermissionDeliveryCommand(JSON.parse(row.envelope_json));
2712
+ const envelope = JSON.parse(row.envelope_json);
2713
+ interruptedPermission = isPermissionDeliveryCommand(envelope);
2714
+ interruptedFeedback = isFeedbackDeliveryCommand(envelope);
2715
+ interruptedLearning = isLearningDeliveryCommand(envelope);
2716
+ interruptedWorkflow = isWorkflowDeliveryCommand(envelope);
1737
2717
  }
1738
2718
  catch { }
1739
2719
  }
1740
2720
  const existingPermissionRecovery = permissionDispatchRecoveryFromFailureCode(row.failure_code ?? undefined);
2721
+ const feedbackRecovery = interruptedFeedback || isFeedbackDispatchRecoveryCode(row.failure_code ?? undefined);
2722
+ const learningRecovery = interruptedLearning
2723
+ || isLearningDispatchRecoveryCode(row.failure_code ?? undefined)
2724
+ || isLearningRetryableFailureCode(row.failure_code ?? undefined);
2725
+ const workflowRecovery = interruptedWorkflow || isWorkflowDispatchRecoveryCode(row.failure_code ?? undefined);
1741
2726
  const permissionCommand = interruptedPermission || existingPermissionRecovery !== undefined;
1742
2727
  const terminalOutbox = !permissionCommand || row.binding_id === null
1743
2728
  ? undefined
@@ -1767,13 +2752,20 @@ export class DeliveryStore {
1767
2752
  : existingPermissionRecovery === undefined
1768
2753
  ? undefined
1769
2754
  : permissionDispatchRecoveryCode(existingPermissionRecovery);
1770
- const ambiguous = row.failure_code === 'dispatch-started' && permissionRecovery === undefined;
1771
- const exhausted = row.attempt_count >= input.maxAttempts && permissionRecovery === undefined;
2755
+ const ambiguous = row.failure_code === 'dispatch-started'
2756
+ && permissionRecovery === undefined && !feedbackRecovery && !learningRecovery && !workflowRecovery;
2757
+ const exhausted = row.attempt_count >= input.maxAttempts
2758
+ && permissionRecovery === undefined && !feedbackRecovery && !learningRecovery && !workflowRecovery;
1772
2759
  const changed = this.database.prepare(`
1773
2760
  UPDATE inbox_messages SET status = ?, next_attempt_at = ?, claimed_by = NULL,
1774
2761
  fencing_token = NULL, lease_until = NULL, failure_code = ?, updated_at = ?
1775
2762
  WHERE id = ? AND status = 'claimed' AND lease_until <= ?
1776
- `).run(ambiguous || exhausted ? 'dead_letter' : 'retry_wait', ambiguous || exhausted ? null : now, ambiguous ? 'dispatch-ambiguous' : permissionRecovery ?? 'lease-expired', now, row.id, now);
2763
+ `).run(ambiguous || exhausted ? 'dead_letter' : 'retry_wait', ambiguous || exhausted ? null : now, ambiguous ? 'dispatch-ambiguous'
2764
+ : permissionRecovery ?? (feedbackRecovery
2765
+ ? feedbackDispatchRecoveryCode
2766
+ : learningRecovery
2767
+ ? learningDispatchRecoveryCode
2768
+ : workflowRecovery ? workflowDispatchRecoveryCode : 'lease-expired'), now, row.id, now);
1777
2769
  if (changed.changes !== 1)
1778
2770
  continue;
1779
2771
  this.database.prepare(`
@@ -1786,12 +2778,18 @@ export class DeliveryStore {
1786
2778
  return recovered.map(id => this.getInbox(id));
1787
2779
  }
1788
2780
  enqueue(input) {
2781
+ return this.enqueueIntent(input, false);
2782
+ }
2783
+ enqueueIntent(input, approvalRouteValidated) {
1789
2784
  this.assertOpen();
1790
2785
  const binding = this.getBinding(input.bindingId);
1791
2786
  if (binding === undefined || binding.status !== 'active') {
1792
2787
  throw new DeliveryStoreError('invalid-intent', 'outbound intent requires an active binding');
1793
2788
  }
1794
2789
  const intent = canonicalIntent(input, binding, this.maxTextBytes);
2790
+ if (intent.format === 'approval' && !approvalRouteValidated) {
2791
+ throw new DeliveryStoreError('invalid-intent', 'approval Outbox requires an exact v2 route fence');
2792
+ }
1795
2793
  if (intent.idempotencyKey.startsWith('inbound:') && intent.idempotencyKey.endsWith(':reply')) {
1796
2794
  const replyEventId = intent.replyToEventId;
1797
2795
  const inbox = replyEventId === undefined
@@ -1836,14 +2834,333 @@ export class DeliveryStore {
1836
2834
  }
1837
2835
  return this.getOutbox(id);
1838
2836
  }
2837
+ /**
2838
+ * Atomically revalidate one immutable Policy route and insert its approval
2839
+ * Outbox. Legacy routes are deliberately not accepted by this boundary.
2840
+ */
2841
+ enqueueApprovalRoute(input) {
2842
+ this.assertOpen();
2843
+ const route = canonicalApprovalDispatchRoute(input.route);
2844
+ return this.transaction(() => {
2845
+ const binding = this.getBinding(route.bindingId);
2846
+ const owner = binding === undefined ? undefined : this.getBindingPrincipal(binding.id);
2847
+ if (binding === undefined || binding.status !== 'active'
2848
+ || binding.version !== route.bindingVersion
2849
+ || binding.generation !== route.bindingGeneration
2850
+ || binding.workspace !== route.workspace
2851
+ || externalPrincipalId(binding.principal) !== route.principal
2852
+ || owner?.status !== 'active' || owner.role !== 'owner'
2853
+ || owner.id !== route.principalRecordId || owner.version !== route.principalVersion
2854
+ || externalPrincipalId(owner.principal) !== route.principal) {
2855
+ throw new DeliveryStoreError('invalid-binding', 'approval route exact owner fence changed');
2856
+ }
2857
+ const intent = canonicalIntent({
2858
+ idempotencyKey: input.intent.idempotencyKey,
2859
+ bindingId: binding.id,
2860
+ target: { conversation: binding.conversation, principal: binding.principal },
2861
+ text: input.intent.text,
2862
+ format: 'approval',
2863
+ approval: input.intent.approval,
2864
+ }, binding, this.maxTextBytes);
2865
+ const intentHash = digest(JSON.stringify(intent));
2866
+ const winnerRow = this.database.prepare(`${outboxSelect} WHERE idempotency_key = ?`)
2867
+ .get(intent.idempotencyKey);
2868
+ if (winnerRow !== undefined) {
2869
+ const receiptRow = this.database.prepare(`
2870
+ SELECT outbox_id, route_version, source_id, binding_id, binding_version, binding_generation,
2871
+ workspace, principal, principal_record_id, principal_version
2872
+ FROM approval_outbox_routes WHERE outbox_id = ?
2873
+ `).get(winnerRow.id);
2874
+ let receipt;
2875
+ try {
2876
+ receipt = receiptRow === undefined ? undefined : approvalRouteFromRow(receiptRow);
2877
+ }
2878
+ catch { }
2879
+ if (winnerRow.intent_hash !== intentHash || receipt === undefined || !sameApprovalRoute(receipt, route)) {
2880
+ throw new DeliveryStoreError('idempotency-conflict', 'approval Outbox winner does not match the exact immutable v2 route and intent');
2881
+ }
2882
+ return outboxFromRow(winnerRow);
2883
+ }
2884
+ const outbox = this.enqueueIntent(intent, true);
2885
+ this.database.prepare(`
2886
+ INSERT INTO approval_outbox_routes(
2887
+ outbox_id, route_version, source_id, binding_id, binding_version, binding_generation,
2888
+ workspace, principal, principal_record_id, principal_version
2889
+ ) VALUES (?, 2, ?, ?, ?, ?, ?, ?, ?, ?)
2890
+ `).run(outbox.id, route.sourceId, route.bindingId, route.bindingVersion, route.bindingGeneration, route.workspace, route.principal, route.principalRecordId, route.principalVersion);
2891
+ return outbox;
2892
+ });
2893
+ }
2894
+ /**
2895
+ * Resolve and enqueue one Host-owned stable owner route under the same
2896
+ * `BEGIN IMMEDIATE` fence as Outbox insertion.
2897
+ *
2898
+ * This closes the `/new` race between an earlier read and insertion. An
2899
+ * existing idempotency winner is returned without mutation (including
2900
+ * `unknown_after_send`) only after both the current route and the winner's
2901
+ * immutable historical route receipt are revalidated.
2902
+ */
2903
+ enqueueOwnerRoute(input) {
2904
+ this.assertOpen();
2905
+ const authority = canonicalOwnerRouteAuthority(input.authority);
2906
+ let sourceId;
2907
+ try {
2908
+ sourceId = canonicalBackgroundSourceId(input.sourceId);
2909
+ }
2910
+ catch {
2911
+ throw new DeliveryStoreError('invalid-intent', 'owner route source id is invalid');
2912
+ }
2913
+ if (typeof input.sourceHash !== 'string' || !/^[a-f0-9]{64}$/u.test(input.sourceHash)
2914
+ || digest(sourceId) !== input.sourceHash) {
2915
+ throw new DeliveryStoreError('invalid-intent', 'owner route source hash is invalid');
2916
+ }
2917
+ let idempotencyKey;
2918
+ try {
2919
+ idempotencyKey = validateBindingText(input.idempotencyKey, 'idempotencyKey', 512);
2920
+ }
2921
+ catch {
2922
+ throw new DeliveryStoreError('invalid-intent', 'owner route idempotency key is invalid');
2923
+ }
2924
+ return this.transaction(() => {
2925
+ const current = this.getActiveBinding(authority.conversation);
2926
+ const owner = current === undefined ? undefined : this.getPrincipal(current.principal);
2927
+ if (current === undefined || current.status !== 'active'
2928
+ || !bindingMatchesOwnerRoute(current, authority)
2929
+ || owner?.status !== 'active' || owner.role !== 'owner') {
2930
+ throw new DeliveryStoreError('invalid-binding', 'active binding does not match owner route authority');
2931
+ }
2932
+ const winnerRow = this.database.prepare(`${outboxSelect} WHERE idempotency_key = ?`)
2933
+ .get(idempotencyKey);
2934
+ const winner = winnerRow === undefined ? undefined : outboxFromRow(winnerRow);
2935
+ const binding = winner === undefined ? current : this.getBinding(winner.intent.bindingId);
2936
+ if (binding === undefined || !bindingMatchesOwnerRoute(binding, authority)) {
2937
+ throw new DeliveryStoreError('idempotency-conflict', 'owner route idempotency winner has another lineage');
2938
+ }
2939
+ let receiptMetadata;
2940
+ if (winner === undefined) {
2941
+ receiptMetadata = ownerRouteReceiptMetadata({ authority, binding, sourceId });
2942
+ }
2943
+ else {
2944
+ const parsed = parseOwnerRouteReceipt(winner);
2945
+ const evidence = parsed.kind === 'route' ? parsed.evidence : undefined;
2946
+ const initialBinding = evidence === undefined ? undefined : this.getBinding(evidence.initialBindingId);
2947
+ const exactReceipt = evidence !== undefined
2948
+ && evidence.authorityId === authority.id
2949
+ && evidence.authorityHash === ownerRouteAuthorityHash(authority)
2950
+ && evidence.bindingVersion === binding.version - (binding.status === 'revoked' ? 1 : 0)
2951
+ && evidence.generation === binding.generation
2952
+ && evidence.minimumGeneration === authority.minimumGeneration
2953
+ && evidence.sourceId === sourceId
2954
+ && evidence.sourceHash === input.sourceHash
2955
+ && initialBinding !== undefined
2956
+ && bindingMatchesOwnerRoute(initialBinding, authority)
2957
+ && evidence.initialBindingVersion
2958
+ === initialBinding.version - (initialBinding.status === 'revoked' ? 1 : 0)
2959
+ && evidence.initialGeneration === initialBinding.generation;
2960
+ if (!exactReceipt || winner.intent.metadata === undefined) {
2961
+ throw new DeliveryStoreError('idempotency-conflict', 'owner route idempotency winner has an invalid immutable route receipt');
2962
+ }
2963
+ receiptMetadata = winner.intent.metadata;
2964
+ }
2965
+ const expected = canonicalIntent({
2966
+ idempotencyKey,
2967
+ bindingId: binding.id,
2968
+ target: { conversation: binding.conversation, principal: binding.principal },
2969
+ text: input.text,
2970
+ format: input.format ?? 'plain',
2971
+ metadata: receiptMetadata,
2972
+ }, binding, this.maxTextBytes);
2973
+ if (winner !== undefined) {
2974
+ if (winner.intentHash !== digest(JSON.stringify(expected))) {
2975
+ throw new DeliveryStoreError('idempotency-conflict', 'owner route idempotency key was reused with a different immutable intent');
2976
+ }
2977
+ return winner;
2978
+ }
2979
+ return this.enqueue(expected);
2980
+ });
2981
+ }
1839
2982
  getOutbox(id) {
1840
2983
  this.assertOpen();
1841
2984
  const row = this.database.prepare(`${outboxSelect} WHERE id = ?`).get(id);
1842
2985
  return row === undefined ? undefined : outboxFromRow(row);
1843
2986
  }
1844
- getApprovalIntent(operationId, bindingId) {
2987
+ getOutboxByIdempotencyKey(idempotencyKeyInput) {
1845
2988
  this.assertOpen();
1846
- const operation = validateBindingText(operationId, 'operationId', 256);
2989
+ const idempotencyKey = validateBindingText(idempotencyKeyInput, 'idempotencyKey', 200);
2990
+ const row = this.database.prepare(`${outboxSelect} WHERE idempotency_key = ?`)
2991
+ .get(idempotencyKey);
2992
+ return row === undefined ? undefined : outboxFromRow(row);
2993
+ }
2994
+ /** Persist the latest desired replacement for one exact durable message. */
2995
+ publishDeliveryPresentation(input) {
2996
+ this.assertOpen();
2997
+ const presentationKey = validateBindingText(input.presentationKey, 'presentationKey', 500);
2998
+ const originalOutboxIdempotencyKey = validateBindingText(input.originalOutboxIdempotencyKey, 'originalOutboxIdempotencyKey', 200);
2999
+ if (!Number.isSafeInteger(input.revision) || input.revision < 1
3000
+ || input.revision > Number.MAX_SAFE_INTEGER) {
3001
+ throw new DeliveryStoreError('invalid-intent', 'presentation revision is invalid');
3002
+ }
3003
+ const presentation = canonicalDeliveryPresentation(input.presentation);
3004
+ const payloadJson = JSON.stringify(presentation);
3005
+ const payloadHash = digest(payloadJson);
3006
+ const now = this.now();
3007
+ return this.transaction(() => {
3008
+ const current = this.database.prepare(`${deliveryPresentationSelect} WHERE presentation_key = ?`)
3009
+ .get(presentationKey);
3010
+ if (current !== undefined) {
3011
+ if (input.revision < current.revision) {
3012
+ throw new DeliveryStoreError('version-conflict', 'presentation revision is older than durable desired state');
3013
+ }
3014
+ if (input.revision === current.revision) {
3015
+ if (current.original_outbox_idempotency_key !== originalOutboxIdempotencyKey
3016
+ || current.payload_hash !== payloadHash || current.payload_json !== payloadJson) {
3017
+ throw new DeliveryStoreError('idempotency-conflict', 'presentation revision was reused with different content');
3018
+ }
3019
+ return deliveryPresentationFromRow(current);
3020
+ }
3021
+ if (current.original_outbox_idempotency_key !== originalOutboxIdempotencyKey) {
3022
+ throw new DeliveryStoreError('idempotency-conflict', 'presentation lifecycle was retargeted to another durable message');
3023
+ }
3024
+ this.database.prepare(`
3025
+ UPDATE delivery_presentations
3026
+ SET revision = ?, payload_hash = ?, payload_json = ?, status = 'pending',
3027
+ next_attempt_at = NULL, claimed_by = NULL, fencing_token = NULL, lease_until = NULL,
3028
+ failure_code = NULL, updated_at = ?
3029
+ WHERE presentation_key = ? AND revision = ?
3030
+ `).run(input.revision, payloadHash, payloadJson, now, presentationKey, current.revision);
3031
+ }
3032
+ else {
3033
+ this.database.prepare(`
3034
+ INSERT INTO delivery_presentations(
3035
+ presentation_key, original_outbox_idempotency_key, revision, payload_hash, payload_json,
3036
+ status, attempt_count, presented_revision, created_at, updated_at)
3037
+ VALUES (?, ?, ?, ?, ?, 'pending', 0, 0, ?, ?)
3038
+ `).run(presentationKey, originalOutboxIdempotencyKey, input.revision, payloadHash, payloadJson, now, now);
3039
+ }
3040
+ const saved = this.database.prepare(`${deliveryPresentationSelect} WHERE presentation_key = ?`)
3041
+ .get(presentationKey);
3042
+ return deliveryPresentationFromRow(saved);
3043
+ });
3044
+ }
3045
+ getDeliveryPresentation(presentationKeyInput) {
3046
+ this.assertOpen();
3047
+ const presentationKey = validateBindingText(presentationKeyInput, 'presentationKey', 500);
3048
+ const row = this.database.prepare(`${deliveryPresentationSelect} WHERE presentation_key = ?`)
3049
+ .get(presentationKey);
3050
+ return row === undefined ? undefined : deliveryPresentationFromRow(row);
3051
+ }
3052
+ claimDeliveryPresentation(input) {
3053
+ this.assertOpen();
3054
+ const ownerId = validateBindingText(input.ownerId, 'ownerId', 500);
3055
+ if (!Number.isSafeInteger(input.leaseMs) || input.leaseMs < 1) {
3056
+ throw new DeliveryStoreError('conflict', 'presentation lease is invalid');
3057
+ }
3058
+ if ((input.excludePresentationKeys?.length ?? 0) > 100) {
3059
+ throw new DeliveryStoreError('conflict', 'presentation exclusions exceed their bound');
3060
+ }
3061
+ const excluded = input.excludePresentationKeys?.map(key => validateBindingText(key, 'presentationKey', 500)) ?? [];
3062
+ const exclusionClause = excluded.length === 0
3063
+ ? ''
3064
+ : `AND presentation_key NOT IN (${excluded.map(() => '?').join(', ')})`;
3065
+ const now = this.now();
3066
+ return this.transaction(() => {
3067
+ // Quarantine bounded poison rows in-place before claiming a later valid
3068
+ // lifecycle. A malformed earliest row must never starve the queue.
3069
+ for (let inspected = 0; inspected < 100; inspected += 1) {
3070
+ const current = this.database.prepare(`${deliveryPresentationSelect}
3071
+ WHERE (status = 'pending'
3072
+ OR (status = 'retry_wait' AND next_attempt_at <= ?)
3073
+ OR (status = 'attempting' AND lease_until <= ?))
3074
+ ${exclusionClause}
3075
+ ORDER BY created_at, presentation_key LIMIT 1`)
3076
+ .get(now, now, ...excluded);
3077
+ if (current === undefined)
3078
+ return undefined;
3079
+ try {
3080
+ deliveryPresentationFromRow(current);
3081
+ }
3082
+ catch {
3083
+ this.database.prepare(`
3084
+ UPDATE delivery_presentations
3085
+ SET status = 'dead', next_attempt_at = NULL, claimed_by = NULL,
3086
+ lease_until = NULL, failure_code = 'presentation-poison-row', updated_at = ?
3087
+ WHERE presentation_key = ? AND revision = ?
3088
+ `).run(now, current.presentation_key, current.revision);
3089
+ continue;
3090
+ }
3091
+ const fencingToken = (current.fencing_token ?? 0) + 1;
3092
+ const changed = this.database.prepare(`
3093
+ UPDATE delivery_presentations
3094
+ SET status = 'attempting', attempt_count = attempt_count + 1,
3095
+ claimed_by = ?, fencing_token = ?, lease_until = ?, next_attempt_at = NULL,
3096
+ failure_code = NULL, updated_at = ?
3097
+ WHERE presentation_key = ? AND revision = ?
3098
+ AND COALESCE(fencing_token, 0) = ?
3099
+ AND (status = 'pending'
3100
+ OR (status = 'retry_wait' AND next_attempt_at <= ?)
3101
+ OR (status = 'attempting' AND lease_until <= ?))
3102
+ `).run(ownerId, fencingToken, now + input.leaseMs, now, current.presentation_key, current.revision, current.fencing_token ?? 0, now, now);
3103
+ if (changed.changes !== 1)
3104
+ throw new DeliveryStoreError('stale-fence', 'presentation claim changed');
3105
+ const claimed = this.database.prepare(`${deliveryPresentationSelect} WHERE presentation_key = ?`)
3106
+ .get(current.presentation_key);
3107
+ return { presentation: deliveryPresentationFromRow(claimed), fencingToken };
3108
+ }
3109
+ return undefined;
3110
+ });
3111
+ }
3112
+ finishDeliveryPresentation(input) {
3113
+ this.assertOpen();
3114
+ const presentationKey = validateBindingText(input.presentationKey, 'presentationKey', 500);
3115
+ const ownerId = validateBindingText(input.ownerId, 'ownerId', 500);
3116
+ if (!Number.isSafeInteger(input.revision) || input.revision < 1
3117
+ || !Number.isSafeInteger(input.fencingToken) || input.fencingToken < 1) {
3118
+ throw new DeliveryStoreError('stale-fence', 'presentation completion fence is invalid');
3119
+ }
3120
+ const providerMessageId = input.providerMessageId === undefined
3121
+ ? undefined
3122
+ : validateBindingText(input.providerMessageId, 'providerMessageId', 512);
3123
+ const failureCode = input.failureCode === undefined
3124
+ ? undefined
3125
+ : validateBindingText(input.failureCode, 'failureCode', 500);
3126
+ if (input.outcome === 'presented' && providerMessageId === undefined) {
3127
+ throw new DeliveryStoreError('conflict', 'presented replacement requires provider message identity');
3128
+ }
3129
+ if (input.outcome !== 'presented' && failureCode === undefined) {
3130
+ throw new DeliveryStoreError('conflict', 'failed replacement requires a failure code');
3131
+ }
3132
+ if (input.outcome === 'retry_wait'
3133
+ && (!Number.isSafeInteger(input.nextAttemptAt) || input.nextAttemptAt < 0)) {
3134
+ throw new DeliveryStoreError('conflict', 'replacement retry time is invalid');
3135
+ }
3136
+ const now = this.now();
3137
+ const changed = this.database.prepare(`
3138
+ UPDATE delivery_presentations
3139
+ SET status = ?, presented_revision = CASE WHEN ? = 'presented' THEN revision ELSE presented_revision END,
3140
+ provider_message_id = COALESCE(?, provider_message_id), failure_code = ?, next_attempt_at = ?,
3141
+ claimed_by = NULL, lease_until = NULL, updated_at = ?
3142
+ WHERE presentation_key = ? AND revision = ? AND status = 'attempting'
3143
+ AND claimed_by = ? AND fencing_token = ?
3144
+ `).run(input.outcome, input.outcome, providerMessageId ?? null, failureCode ?? null, input.outcome === 'retry_wait' ? input.nextAttemptAt : null, now, presentationKey, input.revision, ownerId, input.fencingToken);
3145
+ if (changed.changes !== 1) {
3146
+ throw new DeliveryStoreError('stale-fence', 'presentation completion lost its claim or revision');
3147
+ }
3148
+ return this.getDeliveryPresentation(presentationKey);
3149
+ }
3150
+ /** Exact provider-addressed lookup used only to bind authenticated replies. */
3151
+ getOutboxByProviderMessage(channelInput, accountInput, providerMessageIdInput) {
3152
+ this.assertOpen();
3153
+ const channel = validateBindingText(channelInput, 'channel', 256);
3154
+ const account = validateBindingText(accountInput, 'account', 256);
3155
+ const providerMessageId = validateBindingText(providerMessageIdInput, 'providerMessageId', 512);
3156
+ const row = this.database.prepare(`${outboxSelect}
3157
+ WHERE channel = ? AND account = ? AND provider_message_id = ?`)
3158
+ .get(channel, account, providerMessageId);
3159
+ return row === undefined ? undefined : outboxFromRow(row);
3160
+ }
3161
+ getApprovalIntent(operationId, bindingId) {
3162
+ this.assertOpen();
3163
+ const operation = validateBindingText(operationId, 'operationId', 256);
1847
3164
  const binding = validateBindingText(bindingId, 'bindingId', 256);
1848
3165
  const row = this.database.prepare(`${outboxSelect}
1849
3166
  WHERE json_extract(intent_json, '$.bindingId') = ?
@@ -1973,6 +3290,177 @@ export class DeliveryStore {
1973
3290
  .all(input.bindingId, limit);
1974
3291
  return rows.map(outboxFromRow);
1975
3292
  }
3293
+ /** Recheck a claimed route immediately before provider I/O. */
3294
+ validateClaimedOwnerRoute(recordInput, guard) {
3295
+ this.assertOpen();
3296
+ const record = this.getOutbox(recordInput.id);
3297
+ if (record === undefined || record.status !== 'attempting'
3298
+ || record.fencingToken !== recordInput.fencingToken
3299
+ || record.claimedBy !== recordInput.claimedBy) {
3300
+ return { kind: 'denied', failureCode: 'owner-route-claim-changed' };
3301
+ }
3302
+ const inspected = this.inspectOwnerRouteDispatch(record, guard);
3303
+ return inspected.kind === 'authorized'
3304
+ ? { kind: 'authorized' }
3305
+ : inspected;
3306
+ }
3307
+ /** Recheck a durable owner route before mutating its exact provider message. */
3308
+ validatePresentationOwnerRoute(recordInput, guard) {
3309
+ this.assertOpen();
3310
+ const record = this.getOutbox(recordInput.id);
3311
+ if (record === undefined || record.intentHash !== recordInput.intentHash
3312
+ || record.intent.idempotencyKey !== recordInput.intent.idempotencyKey
3313
+ || record.providerMessageId !== recordInput.providerMessageId) {
3314
+ return { kind: 'denied', failureCode: 'owner-route-message-changed' };
3315
+ }
3316
+ const inspected = this.inspectOwnerRouteDispatch(record, guard);
3317
+ return inspected.kind === 'authorized'
3318
+ ? { kind: 'authorized' }
3319
+ : inspected;
3320
+ }
3321
+ inspectOwnerRouteDispatch(record, guard) {
3322
+ const parsed = parseOwnerRouteReceipt(record);
3323
+ if (parsed.kind === 'not-route')
3324
+ return parsed;
3325
+ if (parsed.kind === 'invalid') {
3326
+ return { kind: 'denied', failureCode: 'owner-route-receipt-invalid' };
3327
+ }
3328
+ const evidence = parsed.evidence;
3329
+ const authorities = new Map();
3330
+ try {
3331
+ for (const input of guard.ownerRoutes) {
3332
+ const authority = canonicalOwnerRouteAuthority(input);
3333
+ if (authorities.has(authority.id)) {
3334
+ return { kind: 'denied', failureCode: 'owner-route-authority-invalid' };
3335
+ }
3336
+ authorities.set(authority.id, authority);
3337
+ }
3338
+ }
3339
+ catch {
3340
+ return { kind: 'denied', failureCode: 'owner-route-authority-invalid' };
3341
+ }
3342
+ const authority = authorities.get(evidence.authorityId);
3343
+ if (authority === undefined) {
3344
+ return { kind: 'denied', failureCode: 'owner-route-authority-revoked' };
3345
+ }
3346
+ if (ownerRouteAuthorityHash(authority) !== evidence.authorityHash
3347
+ || authority.minimumGeneration !== evidence.minimumGeneration) {
3348
+ return { kind: 'denied', failureCode: 'owner-route-authority-changed' };
3349
+ }
3350
+ const binding = this.getBinding(record.intent.bindingId);
3351
+ const initialBinding = this.getBinding(evidence.initialBindingId);
3352
+ if (binding === undefined || initialBinding === undefined) {
3353
+ return { kind: 'denied', failureCode: 'owner-route-receipt-invalid' };
3354
+ }
3355
+ try {
3356
+ const canonical = canonicalIntent(record.intent, binding, this.maxTextBytes);
3357
+ if (digest(JSON.stringify(canonical)) !== record.intentHash
3358
+ || !bindingMatchesOwnerRoute(binding, authority)
3359
+ || !bindingMatchesOwnerRoute(initialBinding, authority)
3360
+ || evidence.bindingVersion !== binding.version - (binding.status === 'revoked' ? 1 : 0)
3361
+ || evidence.generation !== binding.generation
3362
+ || evidence.initialBindingVersion
3363
+ !== initialBinding.version - (initialBinding.status === 'revoked' ? 1 : 0)
3364
+ || evidence.initialGeneration !== initialBinding.generation) {
3365
+ return { kind: 'denied', failureCode: 'owner-route-receipt-invalid' };
3366
+ }
3367
+ }
3368
+ catch {
3369
+ return { kind: 'denied', failureCode: 'owner-route-receipt-invalid' };
3370
+ }
3371
+ const current = this.getActiveBinding(authority.conversation);
3372
+ if (current === undefined) {
3373
+ return { kind: 'denied', failureCode: 'owner-route-binding-missing' };
3374
+ }
3375
+ const owner = this.getPrincipal(current.principal);
3376
+ if (!bindingMatchesOwnerRoute(current, authority)
3377
+ || owner?.status !== 'active' || owner.role !== 'owner') {
3378
+ return { kind: 'denied', failureCode: 'owner-route-binding-drift' };
3379
+ }
3380
+ if (current.generation < evidence.generation
3381
+ || current.generation < authority.minimumGeneration) {
3382
+ return { kind: 'denied', failureCode: 'owner-route-generation-below-floor' };
3383
+ }
3384
+ try {
3385
+ if (!guard.authorize({ authority, sourceId: evidence.sourceId,
3386
+ idempotencyKey: record.intent.idempotencyKey })) {
3387
+ return { kind: 'denied', failureCode: 'owner-route-policy-revoked' };
3388
+ }
3389
+ }
3390
+ catch {
3391
+ return { kind: 'deferred', failureCode: 'owner-route-policy-check-failed' };
3392
+ }
3393
+ return { kind: 'authorized', authority, binding: current, evidence };
3394
+ }
3395
+ ownerRouteIsProvablyUnsent(record) {
3396
+ if (!['pending', 'retry_wait'].includes(record.status)
3397
+ || record.claimedBy !== undefined || record.providerMessageId !== undefined)
3398
+ return false;
3399
+ const row = this.database.prepare(`
3400
+ SELECT COUNT(*) AS count FROM outbox_attempts
3401
+ WHERE outbox_id = ? AND status IN ('accepted', 'unknown_after_send', 'reconciled', 'attempting')
3402
+ `).get(record.id);
3403
+ return row.count === 0;
3404
+ }
3405
+ rebindOwnerRouteBeforeClaim(record, inspected) {
3406
+ if (record.intent.bindingId === inspected.binding.id)
3407
+ return true;
3408
+ const metadata = ownerRouteReceiptMetadata({
3409
+ authority: inspected.authority,
3410
+ binding: inspected.binding,
3411
+ sourceId: inspected.evidence.sourceId,
3412
+ initial: inspected.evidence,
3413
+ });
3414
+ let intent;
3415
+ try {
3416
+ intent = canonicalIntent({
3417
+ ...record.intent,
3418
+ bindingId: inspected.binding.id,
3419
+ target: { conversation: inspected.binding.conversation, principal: inspected.binding.principal },
3420
+ metadata,
3421
+ }, inspected.binding, this.maxTextBytes);
3422
+ }
3423
+ catch {
3424
+ return false;
3425
+ }
3426
+ const intentJson = JSON.stringify(intent);
3427
+ const intentHash = digest(intentJson);
3428
+ return this.database.prepare(`
3429
+ UPDATE outbox_messages
3430
+ SET binding_id = ?, intent_hash = ?, intent_json = ?, channel = ?, account = ?, lane_hash = ?, updated_at = ?
3431
+ WHERE id = ? AND status = ? AND claimed_by IS NULL AND intent_hash = ?
3432
+ `).run(inspected.binding.id, intentHash, intentJson, inspected.binding.conversation.channel, inspected.binding.conversation.account, conversationHash(inspected.binding.conversation), this.now(), record.id, record.status, record.intentHash).changes === 1;
3433
+ }
3434
+ deadLetterOwnerRouteBeforeClaim(record, ownerId, failureCode, now) {
3435
+ if (!['pending', 'retry_wait'].includes(record.status)
3436
+ || !Number.isSafeInteger(record.attemptCount + 1))
3437
+ return false;
3438
+ const attemptNumber = record.attemptCount + 1;
3439
+ const changed = this.database.prepare(`
3440
+ UPDATE outbox_messages
3441
+ SET status = 'dead', attempt_count = attempt_count + 1, next_attempt_at = NULL,
3442
+ claimed_by = NULL, fencing_token = NULL, lease_until = NULL, failure_code = ?, updated_at = ?
3443
+ WHERE id = ? AND status = ? AND claimed_by IS NULL AND intent_hash = ?
3444
+ `).run(failureCode, now, record.id, record.status, record.intentHash);
3445
+ if (changed.changes !== 1)
3446
+ return false;
3447
+ this.database.prepare(`
3448
+ INSERT INTO outbox_attempts (
3449
+ id, outbox_id, attempt_number, owner_id, fencing_token, operation, status,
3450
+ failure_code, created_at, finished_at
3451
+ ) VALUES (?, ?, ?, ?, ?, 'send', 'dead', ?, ?, ?)
3452
+ `).run(`outbox_attempt_${randomUUID()}`, record.id, attemptNumber, ownerId, attemptNumber, failureCode, now, now);
3453
+ return true;
3454
+ }
3455
+ deferOwnerRouteBeforeClaim(record, failureCode, now, retryAt) {
3456
+ if (!['pending', 'retry_wait'].includes(record.status))
3457
+ return false;
3458
+ return this.database.prepare(`
3459
+ UPDATE outbox_messages
3460
+ SET status = 'retry_wait', next_attempt_at = ?, failure_code = ?, updated_at = ?
3461
+ WHERE id = ? AND status = ? AND claimed_by IS NULL AND intent_hash = ?
3462
+ `).run(retryAt, failureCode, now, record.id, record.status, record.intentHash).changes === 1;
3463
+ }
1976
3464
  claimOutbox(input) {
1977
3465
  this.assertOpen();
1978
3466
  const ownerId = validateBindingText(input.ownerId, 'ownerId', 256);
@@ -2005,6 +3493,7 @@ export class DeliveryStore {
2005
3493
  const routeParameters = routes?.flatMap(route => [route.channel, route.account]) ?? [];
2006
3494
  const now = this.now();
2007
3495
  const claims = [];
3496
+ const validatedOwnerRouteIds = [];
2008
3497
  this.transaction(() => {
2009
3498
  this.database.prepare(`
2010
3499
  UPDATE outbox_messages SET status = 'dead', failure_code = 'attempts-exhausted',
@@ -2025,6 +3514,11 @@ export class DeliveryStore {
2025
3514
  WHERE rowid IN (
2026
3515
  SELECT candidate.rowid FROM outbox_messages AS candidate
2027
3516
  WHERE candidate.status = 'unknown_after_send'
3517
+ AND NOT EXISTS (
3518
+ SELECT 1 FROM dead_letter_resolutions AS resolution
3519
+ WHERE resolution.kind = 'outbox' AND resolution.message_id = candidate.id
3520
+ AND resolution.attempt_count = candidate.attempt_count AND resolution.resolution = 'cancel'
3521
+ )
2028
3522
  AND (
2029
3523
  SELECT COUNT(*) FROM outbox_attempts AS history
2030
3524
  WHERE history.outbox_id = candidate.id AND history.operation = 'reconcile'
@@ -2032,6 +3526,40 @@ export class DeliveryStore {
2032
3526
  ORDER BY candidate.rowid LIMIT ?
2033
3527
  )
2034
3528
  `).run(now, input.maxAttempts, maintenanceLimit);
3529
+ const ownerRouteRows = this.database.prepare(`
3530
+ SELECT id FROM outbox_messages
3531
+ WHERE status IN ('pending', 'retry_wait') AND claimed_by IS NULL
3532
+ AND (next_attempt_at IS NULL OR next_attempt_at <= ?)
3533
+ AND json_type(intent_json, '$.metadata."dsh.route.authority"') IS NOT NULL
3534
+ ORDER BY rowid LIMIT ?
3535
+ `).all(now, maintenanceLimit);
3536
+ const ownerRouteGuard = input.ownerRouteGuard ?? { ownerRoutes: [], authorize: () => false };
3537
+ for (const row of ownerRouteRows) {
3538
+ const record = this.getOutbox(row.id);
3539
+ if (record === undefined)
3540
+ continue;
3541
+ const inspected = this.inspectOwnerRouteDispatch(record, ownerRouteGuard);
3542
+ if (inspected.kind === 'deferred') {
3543
+ this.deferOwnerRouteBeforeClaim(record, inspected.failureCode, now, now + Math.max(1, Math.min(input.leaseMs, 30_000)));
3544
+ continue;
3545
+ }
3546
+ if (inspected.kind !== 'authorized') {
3547
+ const failureCode = inspected.kind === 'denied'
3548
+ ? inspected.failureCode
3549
+ : 'owner-route-receipt-invalid';
3550
+ this.deadLetterOwnerRouteBeforeClaim(record, ownerId, failureCode, now);
3551
+ continue;
3552
+ }
3553
+ if (!this.ownerRouteIsProvablyUnsent(record)) {
3554
+ this.deadLetterOwnerRouteBeforeClaim(record, ownerId, 'owner-route-send-proof-missing', now);
3555
+ continue;
3556
+ }
3557
+ if (!this.rebindOwnerRouteBeforeClaim(record, inspected)) {
3558
+ this.deadLetterOwnerRouteBeforeClaim(record, ownerId, 'owner-route-rebind-conflict', now);
3559
+ continue;
3560
+ }
3561
+ validatedOwnerRouteIds.push(record.id);
3562
+ }
2035
3563
  this.database.prepare(`
2036
3564
  UPDATE outbox_messages SET status = 'dead', failure_code = CASE
2037
3565
  WHEN status = 'unknown_after_send' THEN 'binding-revoked-unknown'
@@ -2041,6 +3569,12 @@ export class DeliveryStore {
2041
3569
  WHERE rowid IN (
2042
3570
  SELECT candidate.rowid FROM outbox_messages AS candidate
2043
3571
  WHERE candidate.status IN ('pending', 'retry_wait', 'unknown_after_send')
3572
+ AND json_type(candidate.intent_json, '$.metadata."dsh.route.authority"') IS NULL
3573
+ AND NOT EXISTS (
3574
+ SELECT 1 FROM dead_letter_resolutions AS resolution
3575
+ WHERE resolution.kind = 'outbox' AND resolution.message_id = candidate.id
3576
+ AND resolution.attempt_count = candidate.attempt_count AND resolution.resolution = 'cancel'
3577
+ )
2044
3578
  AND NOT EXISTS (
2045
3579
  SELECT 1 FROM conversation_bindings AS binding
2046
3580
  WHERE binding.id = candidate.binding_id AND binding.status = 'active'
@@ -2048,10 +3582,25 @@ export class DeliveryStore {
2048
3582
  ORDER BY candidate.rowid LIMIT ?
2049
3583
  )
2050
3584
  `).run(now, maintenanceLimit);
3585
+ const ownerRouteClaimClause = validatedOwnerRouteIds.length === 0
3586
+ ? `AND (
3587
+ json_type(candidate.intent_json, '$.metadata."dsh.route.authority"') IS NULL
3588
+ OR candidate.status = 'unknown_after_send'
3589
+ )`
3590
+ : `AND (
3591
+ json_type(candidate.intent_json, '$.metadata."dsh.route.authority"') IS NULL
3592
+ OR candidate.status = 'unknown_after_send'
3593
+ OR candidate.id IN (${validatedOwnerRouteIds.map(() => '?').join(', ')})
3594
+ )`;
2051
3595
  const candidates = this.database.prepare(`
2052
3596
  SELECT candidate.id, candidate.status FROM outbox_messages AS candidate
2053
3597
  WHERE candidate.status IN ('pending', 'retry_wait', 'unknown_after_send')
2054
3598
  AND candidate.claimed_by IS NULL
3599
+ AND NOT EXISTS (
3600
+ SELECT 1 FROM dead_letter_resolutions AS resolution
3601
+ WHERE resolution.kind = 'outbox' AND resolution.message_id = candidate.id
3602
+ AND resolution.attempt_count = candidate.attempt_count AND resolution.resolution = 'cancel'
3603
+ )
2055
3604
  AND (candidate.next_attempt_at IS NULL OR candidate.next_attempt_at <= ?)
2056
3605
  AND (
2057
3606
  candidate.status = 'pending'
@@ -2064,10 +3613,16 @@ export class DeliveryStore {
2064
3613
  WHERE history.outbox_id = candidate.id AND history.operation = 'reconcile'
2065
3614
  ) < ?)
2066
3615
  )
2067
- AND EXISTS (
2068
- SELECT 1 FROM conversation_bindings AS binding
2069
- WHERE binding.id = candidate.binding_id AND binding.status = 'active'
3616
+ AND (
3617
+ EXISTS (
3618
+ SELECT 1 FROM conversation_bindings AS binding
3619
+ WHERE binding.id = candidate.binding_id AND binding.status = 'active'
3620
+ ) OR (
3621
+ candidate.status = 'unknown_after_send'
3622
+ AND json_type(candidate.intent_json, '$.metadata."dsh.route.authority"') IS NOT NULL
3623
+ )
2070
3624
  )
3625
+ ${ownerRouteClaimClause}
2071
3626
  ${routeClause}
2072
3627
  ${excludeClause}
2073
3628
  AND NOT EXISTS (
@@ -2075,6 +3630,11 @@ export class DeliveryStore {
2075
3630
  WHERE earlier.lane_hash = candidate.lane_hash
2076
3631
  AND earlier.rowid < candidate.rowid
2077
3632
  AND earlier.status NOT IN ('accepted', 'delivered', 'read', 'dead')
3633
+ AND NOT EXISTS (
3634
+ SELECT 1 FROM dead_letter_resolutions AS resolution
3635
+ WHERE resolution.kind = 'outbox' AND resolution.message_id = earlier.id
3636
+ AND resolution.attempt_count = earlier.attempt_count AND resolution.resolution = 'cancel'
3637
+ )
2078
3638
  )
2079
3639
  AND NOT EXISTS (
2080
3640
  SELECT 1 FROM outbox_messages AS active
@@ -2082,7 +3642,7 @@ export class DeliveryStore {
2082
3642
  )
2083
3643
  ORDER BY candidate.rowid
2084
3644
  LIMIT ?
2085
- `).all(now, input.maxAttempts, input.maxAttempts, ...routeParameters, ...excludeIds, input.limit);
3645
+ `).all(now, input.maxAttempts, input.maxAttempts, ...validatedOwnerRouteIds, ...routeParameters, ...excludeIds, input.limit);
2086
3646
  for (const candidate of candidates) {
2087
3647
  const current = this.getOutbox(candidate.id);
2088
3648
  const mode = candidate.status === 'unknown_after_send' ? 'reconcile' : 'send';
@@ -2235,7 +3795,12 @@ export class DeliveryStore {
2235
3795
  const current = this.getOutbox(row.id);
2236
3796
  const rank = { accepted: 1, delivered: 2, read: 3 };
2237
3797
  if ((rank[input.status] ?? 0) > (rank[current.status] ?? 0)) {
2238
- this.database.prepare('UPDATE outbox_messages SET status = ?, updated_at = ? WHERE id = ?')
3798
+ this.database.prepare(`
3799
+ UPDATE outbox_messages
3800
+ SET status = ?, failure_code = NULL, next_attempt_at = NULL,
3801
+ claimed_by = NULL, fencing_token = NULL, lease_until = NULL, updated_at = ?
3802
+ WHERE id = ?
3803
+ `)
2239
3804
  .run(input.status, now, row.id);
2240
3805
  }
2241
3806
  });
@@ -2289,47 +3854,110 @@ export class DeliveryStore {
2289
3854
  }
2290
3855
  resolveInbox(input) {
2291
3856
  this.assertOpen();
2292
- const current = this.getInbox(input.inboxId);
2293
- if (current?.status !== 'dead_letter' || current.attemptCount !== input.expectedAttemptCount) {
2294
- throw new DeliveryStoreError('version-conflict', 'dead-letter inbox attempt count or state changed');
2295
- }
2296
- if (input.resolution === 'retry') {
2297
- const binding = current.bindingId === undefined ? undefined : this.getBinding(current.bindingId);
2298
- if (binding?.status !== 'active')
2299
- throw new DeliveryStoreError('conflict', 'dead-letter inbox has no active binding');
3857
+ const operatorId = resolutionOperatorId(input.operatorId);
3858
+ if (!['cancel', 'retry'].includes(input.resolution)
3859
+ || !Number.isSafeInteger(input.expectedAttemptCount) || input.expectedAttemptCount < 0) {
3860
+ throw new DeliveryStoreError('version-conflict', 'dead-letter inbox resolution identity is invalid');
2300
3861
  }
2301
- const now = this.now();
2302
- const changed = this.database.prepare(`
2303
- UPDATE inbox_messages SET status = ?, failure_code = ?, next_attempt_at = NULL, updated_at = ?
2304
- WHERE id = ? AND status = 'dead_letter' AND attempt_count = ?
2305
- `).run(input.resolution === 'retry' ? 'queued' : 'dead_letter', input.resolution === 'retry' ? null : 'operator-cancelled', now, input.inboxId, input.expectedAttemptCount);
2306
- if (changed.changes !== 1)
2307
- throw new DeliveryStoreError('version-conflict', 'dead-letter inbox changed');
2308
- return this.getInbox(input.inboxId);
3862
+ return this.transaction(() => {
3863
+ const current = this.getInbox(input.inboxId);
3864
+ const existing = this.getDeadLetterResolution({ kind: 'inbox', id: input.inboxId,
3865
+ attemptCount: input.expectedAttemptCount });
3866
+ if (existing !== undefined) {
3867
+ if (current !== undefined && existing.resolution === input.resolution
3868
+ && existing.operatorId === operatorId) {
3869
+ return { record: current, receipt: existing, replayed: true };
3870
+ }
3871
+ throw new DeliveryStoreError('version-conflict', 'dead-letter inbox decision already settled');
3872
+ }
3873
+ if (current?.status !== 'dead_letter' || current.attemptCount !== input.expectedAttemptCount) {
3874
+ throw new DeliveryStoreError('version-conflict', 'dead-letter inbox attempt count or state changed');
3875
+ }
3876
+ if (input.resolution === 'retry') {
3877
+ const binding = current.bindingId === undefined ? undefined : this.getBinding(current.bindingId);
3878
+ if (binding?.status !== 'active') {
3879
+ throw new DeliveryStoreError('conflict', 'dead-letter inbox has no active binding');
3880
+ }
3881
+ }
3882
+ const now = this.now();
3883
+ this.database.prepare(`
3884
+ INSERT INTO dead_letter_resolutions (
3885
+ kind, message_id, attempt_count, receipt_version, resolution, original_status,
3886
+ original_failure_code, operator_id, created_at
3887
+ ) VALUES ('inbox', ?, ?, 1, ?, 'dead_letter', ?, ?, ?)
3888
+ `).run(input.inboxId, input.expectedAttemptCount, input.resolution, current.failureCode ?? null, operatorId, now);
3889
+ const changed = this.database.prepare(`
3890
+ UPDATE inbox_messages SET status = ?, failure_code = ?, next_attempt_at = NULL, updated_at = ?
3891
+ WHERE id = ? AND status = 'dead_letter' AND attempt_count = ?
3892
+ `).run(input.resolution === 'retry' ? 'queued' : 'dead_letter', input.resolution === 'retry' ? null : 'operator-cancelled', now, input.inboxId, input.expectedAttemptCount);
3893
+ if (changed.changes !== 1)
3894
+ throw new DeliveryStoreError('version-conflict', 'dead-letter inbox changed');
3895
+ return {
3896
+ record: this.getInbox(input.inboxId),
3897
+ receipt: this.getDeadLetterResolution({ kind: 'inbox', id: input.inboxId,
3898
+ attemptCount: input.expectedAttemptCount }),
3899
+ replayed: false,
3900
+ };
3901
+ });
2309
3902
  }
2310
3903
  resolveOutbox(input) {
2311
3904
  this.assertOpen();
3905
+ const operatorId = resolutionOperatorId(input.operatorId);
3906
+ if (!['cancel', 'retry'].includes(input.resolution)
3907
+ || !Number.isSafeInteger(input.expectedAttemptCount) || input.expectedAttemptCount < 0) {
3908
+ throw new DeliveryStoreError('version-conflict', 'outbox resolution identity is invalid');
3909
+ }
2312
3910
  return this.transaction(() => {
2313
3911
  const current = this.getOutbox(input.outboxId);
3912
+ const existing = this.getDeadLetterResolution({ kind: 'outbox', id: input.outboxId,
3913
+ attemptCount: input.expectedAttemptCount });
3914
+ if (existing !== undefined) {
3915
+ if (current !== undefined && existing.resolution === input.resolution
3916
+ && existing.operatorId === operatorId) {
3917
+ return { record: current, receipt: existing, replayed: true };
3918
+ }
3919
+ throw new DeliveryStoreError('version-conflict', 'outbox decision already settled');
3920
+ }
2314
3921
  if (current === undefined || !['dead', 'unknown_after_send'].includes(current.status)
2315
3922
  || current.attemptCount !== input.expectedAttemptCount) {
2316
3923
  throw new DeliveryStoreError('version-conflict', 'outbox attempt count or resolvable state changed');
2317
3924
  }
2318
3925
  if (input.resolution === 'retry') {
3926
+ if (current.status === 'unknown_after_send'
3927
+ && parseOwnerRouteReceipt(current).kind !== 'not-route') {
3928
+ throw new DeliveryStoreError('conflict', 'unknown owner route sends cannot move lineage or be replayed');
3929
+ }
2319
3930
  const binding = this.getBinding(current.intent.bindingId);
2320
3931
  if (binding?.status !== 'active') {
2321
3932
  throw new DeliveryStoreError('conflict', 'resolvable outbox has no active binding');
2322
3933
  }
2323
3934
  }
2324
3935
  const now = this.now();
3936
+ this.database.prepare(`
3937
+ INSERT INTO dead_letter_resolutions (
3938
+ kind, message_id, attempt_count, receipt_version, resolution, original_status,
3939
+ original_failure_code, operator_id, created_at
3940
+ ) VALUES ('outbox', ?, ?, 1, ?, ?, ?, ?, ?)
3941
+ `).run(input.outboxId, input.expectedAttemptCount, input.resolution, current.status, current.failureCode ?? null, operatorId, now);
3942
+ const targetStatus = input.resolution === 'retry'
3943
+ ? 'pending'
3944
+ : current.status;
3945
+ const targetFailure = input.resolution === 'retry'
3946
+ ? null
3947
+ : current.status === 'unknown_after_send' ? 'operator-cancelled-unknown' : 'operator-cancelled';
2325
3948
  const changed = this.database.prepare(`
2326
3949
  UPDATE outbox_messages SET status = ?, failure_code = ?, next_attempt_at = NULL,
2327
3950
  claimed_by = NULL, fencing_token = NULL, lease_until = NULL, updated_at = ?
2328
3951
  WHERE id = ? AND status = ? AND attempt_count = ?
2329
- `).run(input.resolution === 'retry' ? 'pending' : 'dead', input.resolution === 'retry' ? null : 'operator-cancelled', now, input.outboxId, current.status, input.expectedAttemptCount);
3952
+ `).run(targetStatus, targetFailure, now, input.outboxId, current.status, input.expectedAttemptCount);
2330
3953
  if (changed.changes !== 1)
2331
3954
  throw new DeliveryStoreError('version-conflict', 'resolvable outbox changed');
2332
- return this.getOutbox(input.outboxId);
3955
+ return {
3956
+ record: this.getOutbox(input.outboxId),
3957
+ receipt: this.getDeadLetterResolution({ kind: 'outbox', id: input.outboxId,
3958
+ attemptCount: input.expectedAttemptCount }),
3959
+ replayed: false,
3960
+ };
2333
3961
  });
2334
3962
  }
2335
3963
  beginApprovalSettlement(input) {
@@ -2626,6 +4254,1260 @@ export class DeliveryStore {
2626
4254
  return JSON.parse(completedResultJson);
2627
4255
  });
2628
4256
  }
4257
+ workflowTraceSourceAttestation() {
4258
+ this.assertOpen();
4259
+ const row = this.database.prepare(`
4260
+ SELECT generation, authority_digest FROM workflow_trace_source WHERE singleton = 1
4261
+ `).get();
4262
+ if (row === undefined || !Number.isSafeInteger(row.generation) || row.generation < 1
4263
+ || !/^[a-f0-9]{64}$/u.test(row.authority_digest)) {
4264
+ throw new DeliveryStoreError('conflict', 'workflow trace source authority is corrupt');
4265
+ }
4266
+ return Object.freeze({
4267
+ sourceId: WORKFLOW_TRACE_SOURCE_ID,
4268
+ generation: row.generation,
4269
+ authorityDigest: row.authority_digest,
4270
+ });
4271
+ }
4272
+ /**
4273
+ * Apply one already-authenticated source revision. First version may be any
4274
+ * positive integer and gaps are allowed; lower versions and same-version
4275
+ * content changes fail closed.
4276
+ */
4277
+ recordWorkflowTraceRevision(input) {
4278
+ this.assertOpen();
4279
+ const revision = validateWorkflowTraceRevision(input);
4280
+ return this.transaction(() => this.insertWorkflowTraceRevisionInTransaction(revision));
4281
+ }
4282
+ /**
4283
+ * Atomic owner command ledger + private template review + source revision.
4284
+ * Prompt text is stored only in workflow_template_registry.content_json; the
4285
+ * command receipt and Growth trace carry only references and digests.
4286
+ */
4287
+ commitOwnerWorkflowTraceCommand(input) {
4288
+ this.assertOpen();
4289
+ const operationId = validateBindingText(input.operationId, 'workflow operationId', 512);
4290
+ if (!/^[a-f0-9]{64}$/u.test(input.payloadDigest)
4291
+ || !/^[a-f0-9]{64}$/u.test(input.subjectRef)
4292
+ || !Number.isSafeInteger(input.occurredAt) || input.occurredAt < 0
4293
+ || (input.action === 'upsert') !== (input.templateContent !== undefined
4294
+ && input.steps !== undefined && input.taskRef !== undefined)
4295
+ || (input.taskRef !== undefined && !/^[a-f0-9]{64}$/u.test(input.taskRef))) {
4296
+ throw new DeliveryStoreError('conflict', 'workflow command tuple is invalid');
4297
+ }
4298
+ return this.transaction(() => {
4299
+ const prior = this.database.prepare(`
4300
+ SELECT payload_digest, result_json FROM workflow_trace_commands WHERE operation_id = ?
4301
+ `).get(operationId);
4302
+ if (prior !== undefined) {
4303
+ if (prior.payload_digest !== input.payloadDigest) {
4304
+ throw new DeliveryStoreError('idempotency-conflict', 'workflow command was reused with different input');
4305
+ }
4306
+ let result;
4307
+ try {
4308
+ result = JSON.parse(prior.result_json);
4309
+ }
4310
+ catch {
4311
+ throw new DeliveryStoreError('conflict', 'workflow command receipt is corrupt');
4312
+ }
4313
+ return Object.freeze({
4314
+ revision: validateWorkflowTraceRevision(result.revision),
4315
+ ...(result.template === undefined
4316
+ ? {}
4317
+ : { template: validateWorkflowAutomationTemplate(result.template) }),
4318
+ replayed: true,
4319
+ });
4320
+ }
4321
+ const binding = this.getBinding(input.binding.id);
4322
+ const owner = binding === undefined ? undefined : this.getPrincipal(binding.principal);
4323
+ if (binding === undefined || binding.status !== 'active'
4324
+ || binding.version !== input.binding.version || binding.generation !== input.binding.generation
4325
+ || JSON.stringify(binding.conversation) !== JSON.stringify(input.binding.conversation)
4326
+ || JSON.stringify(binding.principal) !== JSON.stringify(input.binding.principal)
4327
+ || binding.workspace !== input.binding.workspace || binding.agentPreset !== input.binding.agentPreset
4328
+ || owner?.status !== 'active' || owner.role !== 'owner') {
4329
+ throw new DeliveryStoreError('unauthorized-principal', 'workflow command owner binding changed');
4330
+ }
4331
+ const reviewInbox = this.getInbox(input.reviewInboxId);
4332
+ const sourceInbox = this.getInbox(input.sourceInboxId);
4333
+ const sourceOutbox = this.getOutbox(input.sourceOutboxId);
4334
+ if (reviewInbox?.status !== 'claimed' || reviewInbox.bindingId !== binding.id
4335
+ || sourceInbox?.status !== 'processed' || sourceInbox.bindingId !== binding.id
4336
+ || sourceOutbox === undefined || sourceOutbox.intent.bindingId !== binding.id
4337
+ || sourceOutbox.intent.replyToEventId !== sourceInbox.envelope.eventId
4338
+ || sourceOutbox.providerMessageId === undefined
4339
+ || !['accepted', 'delivered', 'read'].includes(sourceOutbox.status)
4340
+ || JSON.stringify(sourceInbox.envelope.conversation) !== JSON.stringify(binding.conversation)
4341
+ || JSON.stringify(sourceInbox.envelope.principal) !== JSON.stringify(binding.principal)
4342
+ || JSON.stringify(sourceOutbox.intent.target.conversation) !== JSON.stringify(binding.conversation)
4343
+ || JSON.stringify(sourceOutbox.intent.target.principal) !== JSON.stringify(binding.principal)) {
4344
+ throw new DeliveryStoreError('conflict', 'workflow command does not target one completed owner task');
4345
+ }
4346
+ const currentRow = this.database.prepare(`
4347
+ SELECT revision.* FROM workflow_trace_current AS current
4348
+ JOIN workflow_trace_revisions AS revision
4349
+ ON revision.subject_ref = current.subject_ref AND revision.version = current.version
4350
+ WHERE current.subject_ref = ?
4351
+ `).get(input.subjectRef);
4352
+ const current = currentRow === undefined ? undefined : workflowTraceRevision(currentRow);
4353
+ const version = current === undefined ? 1 : current.version + 1;
4354
+ if (!Number.isSafeInteger(version)) {
4355
+ throw new DeliveryStoreError('version-conflict', 'workflow trace version overflow');
4356
+ }
4357
+ let template;
4358
+ if (input.action === 'upsert') {
4359
+ const content = validateWorkflowAutomationTemplateContent({
4360
+ ...input.templateContent,
4361
+ principalId: externalPrincipalId(binding.principal),
4362
+ });
4363
+ if (content.scope.workspace !== binding.workspace || content.scope.preset !== binding.agentPreset
4364
+ || content.ownerBindingId !== binding.id || content.deliveryBindingId !== binding.id) {
4365
+ throw new DeliveryStoreError('invalid-binding', 'workflow template does not match the owner binding');
4366
+ }
4367
+ const templateDigest = workflowAutomationTemplateContentDigest(content);
4368
+ const reviewReceipt = Object.freeze({
4369
+ contractVersion: 1,
4370
+ kind: 'owner-explicit-template-review',
4371
+ limitation: 'deidentification-unproven',
4372
+ templateDigest,
4373
+ bindingId: binding.id,
4374
+ bindingVersion: binding.version,
4375
+ bindingGeneration: binding.generation,
4376
+ principalId: owner.id,
4377
+ reviewInboxId: reviewInbox.id,
4378
+ sourceInboxId: sourceInbox.id,
4379
+ sourceOutboxId: sourceOutbox.id,
4380
+ });
4381
+ const attestationDigest = growthObjectDigest({
4382
+ contract: 'assistant-delivery-workflow-template-review/v1',
4383
+ receipt: reviewReceipt,
4384
+ });
4385
+ const templateRef = `workflow-template:${growthObjectDigest({
4386
+ contract: 'assistant-delivery-workflow-template-ref/v1',
4387
+ templateDigest,
4388
+ reviewInboxId: reviewInbox.id,
4389
+ })}`;
4390
+ const attestationId = `workflow-review:${attestationDigest}`;
4391
+ template = validateWorkflowAutomationTemplate({
4392
+ templateRef,
4393
+ templateDigest,
4394
+ privacyAttestation: {
4395
+ kind: 'owner-explicit',
4396
+ limitation: 'deidentification-unproven',
4397
+ attestationId,
4398
+ attestationDigest,
4399
+ },
4400
+ });
4401
+ this.revokeCurrentWorkflowTemplateInTransaction(current, this.now());
4402
+ const now = this.now();
4403
+ this.database.prepare(`
4404
+ INSERT INTO workflow_template_registry(
4405
+ template_ref, template_digest, scope_key, workspace, preset, owner_binding_id,
4406
+ content_json, privacy_kind, privacy_attestation_id, privacy_attestation_digest,
4407
+ review_receipt_json, status, review_inbox_id, source_inbox_id, source_outbox_id,
4408
+ created_at, updated_at, version
4409
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, 'owner-explicit', ?, ?, ?, 'active', ?, ?, ?, ?, ?, 1)
4410
+ `).run(template.templateRef, template.templateDigest, workflowScopeKey(content.scope), content.scope.workspace, content.scope.preset, binding.id, JSON.stringify(content), template.privacyAttestation.attestationId, template.privacyAttestation.attestationDigest, JSON.stringify(reviewReceipt), reviewInbox.id, sourceInbox.id, sourceOutbox.id, now, now);
4411
+ }
4412
+ else {
4413
+ this.revokeCurrentWorkflowTemplateInTransaction(current, this.now());
4414
+ }
4415
+ const payload = Object.freeze({
4416
+ source: this.workflowTraceSourceAttestation(),
4417
+ scope: Object.freeze({ workspace: binding.workspace, preset: binding.agentPreset }),
4418
+ subjectRef: input.subjectRef,
4419
+ version,
4420
+ disposition: input.action,
4421
+ ...(input.action === 'retract' ? {} : {
4422
+ evidence: Object.freeze({
4423
+ occurredAt: input.occurredAt,
4424
+ signal: 'owner-explicit',
4425
+ objectiveStatus: 'unknown',
4426
+ ownerBindingId: binding.id,
4427
+ taskRef: input.taskRef,
4428
+ template: template,
4429
+ steps: input.steps,
4430
+ }),
4431
+ }),
4432
+ });
4433
+ const revision = validateWorkflowTraceRevision({
4434
+ ...payload,
4435
+ digest: workflowTraceRevisionDigest(payload),
4436
+ });
4437
+ const recorded = this.insertWorkflowTraceRevisionInTransaction(revision);
4438
+ if (recorded.replayed) {
4439
+ throw new DeliveryStoreError('version-conflict', 'new workflow command unexpectedly replayed a trace version');
4440
+ }
4441
+ const resultJson = JSON.stringify({ revision, ...(template === undefined ? {} : { template }) });
4442
+ this.database.prepare(`
4443
+ INSERT INTO workflow_trace_commands(operation_id, payload_digest, result_json, created_at)
4444
+ VALUES (?, ?, ?, ?)
4445
+ `).run(operationId, input.payloadDigest, resultJson, this.now());
4446
+ return Object.freeze({ revision, ...(template === undefined ? {} : { template }), replayed: false });
4447
+ });
4448
+ }
4449
+ /**
4450
+ * Atomically record an authenticated owner objective judgement about one
4451
+ * ordinary completed Agent turn and, only when a closed deterministic
4452
+ * deidentification catalog recognizes the source, emit its verified trace.
4453
+ *
4454
+ * The caller may not supply a prompt, task reference, template, or proof:
4455
+ * all of them are rebuilt below from the exact durable Inbox/Outbox fence.
4456
+ * A normal durable reply alone is intentionally insufficient; the owner
4457
+ * must have replied `/feedback achieved` to that exact provider message.
4458
+ */
4459
+ commitVerifiedWorkflowTraceFeedback(input) {
4460
+ this.assertOpen();
4461
+ if (typeof input !== 'object' || input === null
4462
+ || !['achieved', 'partial', 'not-achieved'].includes(input.objectiveStatus)
4463
+ || typeof input.feedbackInboxId !== 'string' || typeof input.sourceInboxId !== 'string'
4464
+ || typeof input.sourceOutboxId !== 'string') {
4465
+ throw new DeliveryStoreError('conflict', 'verified workflow feedback tuple is invalid');
4466
+ }
4467
+ const feedbackInboxId = validateBindingText(input.feedbackInboxId, 'workflow feedback inbox id', 256);
4468
+ const sourceInboxId = validateBindingText(input.sourceInboxId, 'workflow source inbox id', 256);
4469
+ const sourceOutboxId = validateBindingText(input.sourceOutboxId, 'workflow source outbox id', 256);
4470
+ return this.transaction(() => {
4471
+ const binding = this.getBinding(input.binding.id);
4472
+ const owner = binding === undefined ? undefined : this.getPrincipal(binding.principal);
4473
+ if (binding === undefined || binding.status !== 'active' || binding.conversation.kind !== 'dm'
4474
+ || binding.version !== input.binding.version || binding.generation !== input.binding.generation
4475
+ || binding.sessionId !== input.binding.sessionId
4476
+ || JSON.stringify(binding.conversation) !== JSON.stringify(input.binding.conversation)
4477
+ || JSON.stringify(binding.principal) !== JSON.stringify(input.binding.principal)
4478
+ || binding.workspace !== input.binding.workspace || binding.agentPreset !== input.binding.agentPreset
4479
+ || owner?.status !== 'active' || owner.role !== 'owner') {
4480
+ throw new DeliveryStoreError('unauthorized-principal', 'verified workflow owner binding changed');
4481
+ }
4482
+ const feedbackInbox = this.getInbox(feedbackInboxId);
4483
+ const sourceInbox = this.getInbox(sourceInboxId);
4484
+ const sourceOutbox = this.getOutbox(sourceOutboxId);
4485
+ const feedbackCommand = feedbackInbox === undefined || feedbackInbox.envelope.kind !== 'command'
4486
+ ? undefined
4487
+ : parseDeliveryCommand(feedbackInbox.envelope);
4488
+ const feedback = feedbackCommand?.name !== 'feedback'
4489
+ ? undefined
4490
+ : parseFeedbackCommand(feedbackCommand.rawInput);
4491
+ if (feedbackInbox?.status !== 'claimed' || feedbackInbox.bindingId !== binding.id
4492
+ || (feedbackInbox.envelope.attachments?.length ?? 0) !== 0
4493
+ || feedback?.kind !== 'objective' || feedback.objectiveStatus !== input.objectiveStatus
4494
+ || sourceInbox?.status !== 'processed' || sourceInbox.bindingId !== binding.id
4495
+ || sourceInbox.envelope.kind !== 'text' || (sourceInbox.envelope.attachments?.length ?? 0) !== 0
4496
+ || sourceOutbox === undefined || sourceOutbox.intent.bindingId !== binding.id
4497
+ || sourceOutbox.intent.idempotencyKey !== `inbound:${sourceInbox.id}:reply`
4498
+ || sourceOutbox.intent.replyToEventId !== sourceInbox.envelope.eventId
4499
+ || !['accepted', 'delivered', 'read'].includes(sourceOutbox.status)
4500
+ || sourceOutbox.providerMessageId === undefined
4501
+ || (sourceOutbox.intent.format !== 'markdown' && sourceOutbox.intent.format !== 'plain')
4502
+ || sourceOutbox.intent.metadata !== undefined
4503
+ || sourceOutbox.intent.modelPicker !== undefined
4504
+ || sourceOutbox.intent.permissionPicker !== undefined
4505
+ || sourceOutbox.intent.approval !== undefined
4506
+ || feedbackInbox.envelope.metadata?.replyToProviderMessageId !== sourceOutbox.providerMessageId
4507
+ || JSON.stringify(feedbackInbox.envelope.conversation) !== JSON.stringify(binding.conversation)
4508
+ || JSON.stringify(feedbackInbox.envelope.principal) !== JSON.stringify(binding.principal)
4509
+ || JSON.stringify(sourceInbox.envelope.conversation) !== JSON.stringify(binding.conversation)
4510
+ || JSON.stringify(sourceInbox.envelope.principal) !== JSON.stringify(binding.principal)
4511
+ || JSON.stringify(sourceOutbox.intent.target.conversation) !== JSON.stringify(binding.conversation)
4512
+ || JSON.stringify(sourceOutbox.intent.target.principal) !== JSON.stringify(binding.principal)) {
4513
+ throw new DeliveryStoreError('conflict', 'verified workflow feedback does not target one completed owner turn');
4514
+ }
4515
+ const taskRef = growthObjectDigest({
4516
+ contract: 'assistant-delivery-verified-workflow-task-ref/v1',
4517
+ bindingId: binding.id,
4518
+ sourceInboxId: sourceInbox.id,
4519
+ sourceInboxEnvelopeHash: sourceInbox.envelopeHash,
4520
+ sourceOutboxId: sourceOutbox.id,
4521
+ sourceOutboxIntentHash: sourceOutbox.intentHash,
4522
+ });
4523
+ const taskEvidenceDigest = growthObjectDigest({
4524
+ contract: 'assistant-delivery-verified-workflow-task-evidence/v1',
4525
+ binding: {
4526
+ id: binding.id,
4527
+ version: binding.version,
4528
+ generation: binding.generation,
4529
+ workspace: binding.workspace,
4530
+ preset: binding.agentPreset,
4531
+ },
4532
+ principal: { recordId: owner.id, version: owner.version },
4533
+ source: {
4534
+ inboxId: sourceInbox.id,
4535
+ envelopeHash: sourceInbox.envelopeHash,
4536
+ outboxId: sourceOutbox.id,
4537
+ intentHash: sourceOutbox.intentHash,
4538
+ providerMessageId: sourceOutbox.providerMessageId,
4539
+ },
4540
+ feedback: {
4541
+ inboxId: feedbackInbox.id,
4542
+ envelopeHash: feedbackInbox.envelopeHash,
4543
+ objectiveStatus: input.objectiveStatus,
4544
+ },
4545
+ });
4546
+ const prior = this.database.prepare(`
4547
+ SELECT * FROM workflow_verified_task_feedback WHERE source_outbox_id = ?
4548
+ `).get(sourceOutbox.id);
4549
+ if (prior !== undefined) {
4550
+ return this.replayVerifiedWorkflowTraceFeedback({
4551
+ row: prior,
4552
+ binding,
4553
+ ownerId: owner.id,
4554
+ feedbackInboxId: feedbackInbox.id,
4555
+ sourceInboxId: sourceInbox.id,
4556
+ sourceOutboxId: sourceOutbox.id,
4557
+ objectiveStatus: input.objectiveStatus,
4558
+ taskRef,
4559
+ taskEvidenceDigest,
4560
+ });
4561
+ }
4562
+ const descriptor = input.objectiveStatus === 'achieved'
4563
+ ? deriveDeterministicallyDeidentifiedWorkflowTemplate(sourceInbox.envelope.text)
4564
+ : undefined;
4565
+ if (descriptor === undefined) {
4566
+ this.insertVerifiedWorkflowTaskFeedback({
4567
+ sourceOutboxId: sourceOutbox.id,
4568
+ sourceInboxId: sourceInbox.id,
4569
+ feedbackInboxId: feedbackInbox.id,
4570
+ binding,
4571
+ ownerId: owner.id,
4572
+ objectiveStatus: input.objectiveStatus,
4573
+ taskRef,
4574
+ taskEvidenceDigest,
4575
+ });
4576
+ return Object.freeze({
4577
+ outcome: 'no-trace',
4578
+ reason: input.objectiveStatus === 'achieved' ? 'privacy-abstained' : 'objective-not-achieved',
4579
+ replayed: false,
4580
+ });
4581
+ }
4582
+ const content = validateWorkflowAutomationTemplateContent({
4583
+ scope: { workspace: binding.workspace, preset: binding.agentPreset },
4584
+ ownerBindingId: binding.id,
4585
+ principalId: externalPrincipalId(binding.principal),
4586
+ name: descriptor.name,
4587
+ prompt: descriptor.prompt,
4588
+ schedule: descriptor.schedule,
4589
+ timeoutMs: descriptor.timeoutMs,
4590
+ toolCatalogIds: descriptor.toolCatalogIds,
4591
+ deliveryBindingId: binding.id,
4592
+ });
4593
+ const templateDigest = workflowAutomationTemplateContentDigest(content);
4594
+ const privacyReceipt = Object.freeze({
4595
+ contractVersion: 1,
4596
+ kind: 'deterministic-deidentification-template-attestation',
4597
+ method: 'assistant-delivery-redaction-v1',
4598
+ catalogId: descriptor.catalogId,
4599
+ templateDigest,
4600
+ bindingId: binding.id,
4601
+ bindingVersion: binding.version,
4602
+ bindingGeneration: binding.generation,
4603
+ principalId: owner.id,
4604
+ });
4605
+ const attestationDigest = growthObjectDigest({
4606
+ contract: 'assistant-delivery-workflow-template-deidentification/v1',
4607
+ receipt: privacyReceipt,
4608
+ });
4609
+ const template = validateWorkflowAutomationTemplate({
4610
+ templateRef: `workflow-template:${growthObjectDigest({
4611
+ contract: 'assistant-delivery-workflow-template-ref/v2', templateDigest,
4612
+ })}`,
4613
+ templateDigest,
4614
+ privacyAttestation: {
4615
+ kind: 'deterministic-deidentification',
4616
+ method: 'assistant-delivery-redaction-v1',
4617
+ attestationId: `workflow-deidentification:${attestationDigest}`,
4618
+ attestationDigest,
4619
+ },
4620
+ });
4621
+ const existingTemplate = this.getWorkflowAutomationTemplate(template);
4622
+ if (existingTemplate !== undefined) {
4623
+ if (existingTemplate.status !== 'active'
4624
+ || JSON.stringify(existingTemplate.resolved) !== JSON.stringify({ contractVersion: 1, template, ...content })
4625
+ || existingTemplate.review.bindingId !== binding.id
4626
+ || existingTemplate.review.bindingVersion !== binding.version
4627
+ || existingTemplate.review.bindingGeneration !== binding.generation
4628
+ || existingTemplate.review.principalId !== owner.id) {
4629
+ throw new DeliveryStoreError('receipt-mismatch', 'deterministic workflow template is stale');
4630
+ }
4631
+ }
4632
+ else {
4633
+ const now = this.now();
4634
+ this.database.prepare(`
4635
+ INSERT INTO workflow_template_registry(
4636
+ template_ref, template_digest, scope_key, workspace, preset, owner_binding_id,
4637
+ content_json, privacy_kind, privacy_attestation_id, privacy_attestation_digest,
4638
+ review_receipt_json, status, review_inbox_id, source_inbox_id, source_outbox_id,
4639
+ created_at, updated_at, version
4640
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, 'deterministic-deidentification', ?, ?, ?,
4641
+ 'active', ?, ?, ?, ?, ?, 1)
4642
+ `).run(template.templateRef, template.templateDigest, workflowScopeKey(content.scope), content.scope.workspace, content.scope.preset, binding.id, JSON.stringify(content), template.privacyAttestation.attestationId, template.privacyAttestation.attestationDigest, JSON.stringify(privacyReceipt), feedbackInbox.id, sourceInbox.id, sourceOutbox.id, now, now);
4643
+ }
4644
+ const subjectRef = growthObjectDigest({
4645
+ contract: 'assistant-delivery-verified-workflow-subject/v1',
4646
+ bindingId: binding.id,
4647
+ sourceInboxId: sourceInbox.id,
4648
+ sourceOutboxId: sourceOutbox.id,
4649
+ });
4650
+ const payload = Object.freeze({
4651
+ source: this.workflowTraceSourceAttestation(),
4652
+ scope: Object.freeze({ workspace: binding.workspace, preset: binding.agentPreset }),
4653
+ subjectRef,
4654
+ version: 1,
4655
+ disposition: 'upsert',
4656
+ evidence: Object.freeze({
4657
+ occurredAt: feedbackInbox.receivedAt,
4658
+ signal: 'verified-repetition',
4659
+ objectiveStatus: 'achieved',
4660
+ ownerBindingId: binding.id,
4661
+ taskRef,
4662
+ taskEvidenceDigest,
4663
+ template,
4664
+ steps: Object.freeze([Object.freeze({
4665
+ catalogId: 'assistant.agent-turn',
4666
+ argumentSchemaDigest: workflowArgumentShapeDigest({ prompt: descriptor.prompt }),
4667
+ })]),
4668
+ }),
4669
+ });
4670
+ const revision = validateWorkflowTraceRevision({
4671
+ ...payload,
4672
+ digest: workflowTraceRevisionDigest(payload),
4673
+ });
4674
+ const recorded = this.insertWorkflowTraceRevisionInTransaction(revision);
4675
+ if (recorded.replayed) {
4676
+ throw new DeliveryStoreError('version-conflict', 'new verified workflow feedback unexpectedly replayed');
4677
+ }
4678
+ this.insertVerifiedWorkflowTaskFeedback({
4679
+ sourceOutboxId: sourceOutbox.id,
4680
+ sourceInboxId: sourceInbox.id,
4681
+ feedbackInboxId: feedbackInbox.id,
4682
+ binding,
4683
+ ownerId: owner.id,
4684
+ objectiveStatus: input.objectiveStatus,
4685
+ taskRef,
4686
+ taskEvidenceDigest,
4687
+ revision,
4688
+ template,
4689
+ });
4690
+ return Object.freeze({ outcome: 'trace-recorded', revision, template, replayed: false });
4691
+ });
4692
+ }
4693
+ insertVerifiedWorkflowTaskFeedback(input) {
4694
+ if ((input.revision === undefined) !== (input.template === undefined)) {
4695
+ throw new DeliveryStoreError('conflict', 'verified workflow trace and template must agree');
4696
+ }
4697
+ if (input.revision !== undefined && (input.revision.disposition !== 'upsert'
4698
+ || input.revision.evidence?.signal !== 'verified-repetition'
4699
+ || input.revision.evidence.objectiveStatus !== 'achieved'
4700
+ || input.revision.evidence.taskRef !== input.taskRef
4701
+ || input.revision.evidence.taskEvidenceDigest !== input.taskEvidenceDigest
4702
+ || JSON.stringify(input.revision.evidence.template) !== JSON.stringify(input.template))) {
4703
+ throw new DeliveryStoreError('receipt-mismatch', 'verified workflow trace is not bound to its task receipt');
4704
+ }
4705
+ this.database.prepare(`
4706
+ INSERT INTO workflow_verified_task_feedback(
4707
+ source_outbox_id, source_inbox_id, feedback_inbox_id, binding_id, binding_version,
4708
+ binding_generation, principal_record_id, objective_status, task_ref, task_evidence_digest,
4709
+ trace_subject_ref, trace_version, trace_digest, template_ref, created_at
4710
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
4711
+ `).run(input.sourceOutboxId, input.sourceInboxId, input.feedbackInboxId, input.binding.id, input.binding.version, input.binding.generation, input.ownerId, input.objectiveStatus, input.taskRef, input.taskEvidenceDigest, input.revision?.subjectRef ?? null, input.revision?.version ?? null, input.revision?.digest ?? null, input.template?.templateRef ?? null, this.now());
4712
+ }
4713
+ replayVerifiedWorkflowTraceFeedback(input) {
4714
+ const row = input.row;
4715
+ if (row.source_outbox_id !== input.sourceOutboxId || row.source_inbox_id !== input.sourceInboxId
4716
+ || row.feedback_inbox_id !== input.feedbackInboxId || row.binding_id !== input.binding.id
4717
+ || row.binding_version !== input.binding.version || row.binding_generation !== input.binding.generation
4718
+ || row.principal_record_id !== input.ownerId || row.objective_status !== input.objectiveStatus
4719
+ || row.task_ref !== input.taskRef || row.task_evidence_digest !== input.taskEvidenceDigest) {
4720
+ throw new DeliveryStoreError('idempotency-conflict', 'verified workflow task already has a different owner judgement');
4721
+ }
4722
+ const traceFields = [row.trace_subject_ref, row.trace_version, row.trace_digest, row.template_ref];
4723
+ const traceFieldCount = traceFields.filter(value => value !== null).length;
4724
+ if (traceFieldCount === 0) {
4725
+ return Object.freeze({
4726
+ outcome: 'no-trace',
4727
+ reason: row.objective_status === 'achieved' ? 'privacy-abstained' : 'objective-not-achieved',
4728
+ replayed: true,
4729
+ });
4730
+ }
4731
+ if (traceFieldCount !== traceFields.length || row.trace_subject_ref === null || row.trace_version === null
4732
+ || row.trace_digest === null || row.template_ref === null
4733
+ || !Number.isSafeInteger(row.trace_version) || row.trace_version < 1
4734
+ || !/^[a-f0-9]{64}$/u.test(row.trace_subject_ref)
4735
+ || !/^[a-f0-9]{64}$/u.test(row.trace_digest)) {
4736
+ throw new DeliveryStoreError('conflict', 'verified workflow task receipt is corrupt');
4737
+ }
4738
+ const traceRow = this.database.prepare(`
4739
+ SELECT * FROM workflow_trace_revisions WHERE subject_ref = ? AND version = ?
4740
+ `).get(row.trace_subject_ref, row.trace_version);
4741
+ if (traceRow === undefined) {
4742
+ throw new DeliveryStoreError('conflict', 'verified workflow task trace is missing');
4743
+ }
4744
+ const revision = workflowTraceRevision(traceRow);
4745
+ const template = revision.disposition === 'upsert' ? revision.evidence?.template : undefined;
4746
+ if (revision.digest !== row.trace_digest || revision.disposition !== 'upsert'
4747
+ || revision.evidence?.signal !== 'verified-repetition'
4748
+ || revision.evidence.objectiveStatus !== 'achieved'
4749
+ || revision.evidence.ownerBindingId !== input.binding.id
4750
+ || revision.evidence.taskRef !== input.taskRef
4751
+ || revision.evidence.taskEvidenceDigest !== input.taskEvidenceDigest
4752
+ || template === undefined || template.templateRef !== row.template_ref) {
4753
+ throw new DeliveryStoreError('receipt-mismatch', 'verified workflow task trace changed identity');
4754
+ }
4755
+ const storedTemplate = this.getWorkflowAutomationTemplate(template);
4756
+ if (storedTemplate === undefined
4757
+ || JSON.stringify(storedTemplate.resolved.template) !== JSON.stringify(template)) {
4758
+ throw new DeliveryStoreError('receipt-mismatch', 'verified workflow task template is missing');
4759
+ }
4760
+ return Object.freeze({ outcome: 'trace-recorded', revision, template, replayed: true });
4761
+ }
4762
+ getWorkflowAutomationTemplate(templateInput) {
4763
+ this.assertOpen();
4764
+ const template = validateWorkflowAutomationTemplate(templateInput);
4765
+ const row = this.database.prepare(`
4766
+ SELECT * FROM workflow_template_registry WHERE template_ref = ?
4767
+ `).get(template.templateRef);
4768
+ if (row === undefined)
4769
+ return undefined;
4770
+ let contentValue;
4771
+ let reviewValue;
4772
+ try {
4773
+ contentValue = JSON.parse(row.content_json);
4774
+ reviewValue = JSON.parse(row.review_receipt_json);
4775
+ }
4776
+ catch {
4777
+ throw new DeliveryStoreError('conflict', 'workflow template registry payload is corrupt');
4778
+ }
4779
+ if (typeof reviewValue !== 'object' || reviewValue === null || Array.isArray(reviewValue)) {
4780
+ throw new DeliveryStoreError('conflict', 'workflow template review receipt is corrupt');
4781
+ }
4782
+ const review = reviewValue;
4783
+ const content = validateWorkflowAutomationTemplateContent(contentValue);
4784
+ const receiptBindingVersion = review['bindingVersion'];
4785
+ const receiptBindingGeneration = review['bindingGeneration'];
4786
+ let attestationDigest;
4787
+ if (row.privacy_kind === 'owner-explicit') {
4788
+ const exactReviewKeys = [
4789
+ 'contractVersion', 'kind', 'limitation', 'templateDigest', 'bindingId', 'bindingVersion',
4790
+ 'bindingGeneration', 'principalId', 'reviewInboxId', 'sourceInboxId', 'sourceOutboxId',
4791
+ ].sort();
4792
+ if (Object.keys(review).sort().some((key, index) => key !== exactReviewKeys[index])
4793
+ || Object.keys(review).length !== exactReviewKeys.length
4794
+ || review['contractVersion'] !== 1 || review['kind'] !== 'owner-explicit-template-review'
4795
+ || review['limitation'] !== 'deidentification-unproven'
4796
+ || review['templateDigest'] !== row.template_digest
4797
+ || review['bindingId'] !== row.owner_binding_id
4798
+ || review['reviewInboxId'] !== row.review_inbox_id
4799
+ || review['sourceInboxId'] !== row.source_inbox_id
4800
+ || review['sourceOutboxId'] !== row.source_outbox_id
4801
+ || typeof review['principalId'] !== 'string'
4802
+ || typeof receiptBindingVersion !== 'number' || !Number.isSafeInteger(receiptBindingVersion)
4803
+ || receiptBindingVersion < 1
4804
+ || typeof receiptBindingGeneration !== 'number' || !Number.isSafeInteger(receiptBindingGeneration)
4805
+ || receiptBindingGeneration < 1) {
4806
+ throw new DeliveryStoreError('conflict', 'workflow template review receipt columns do not match');
4807
+ }
4808
+ attestationDigest = growthObjectDigest({
4809
+ contract: 'assistant-delivery-workflow-template-review/v1',
4810
+ receipt: review,
4811
+ });
4812
+ }
4813
+ else if (row.privacy_kind === 'deterministic-deidentification') {
4814
+ const exactReceiptKeys = [
4815
+ 'contractVersion', 'kind', 'method', 'catalogId', 'templateDigest', 'bindingId',
4816
+ 'bindingVersion', 'bindingGeneration', 'principalId',
4817
+ ].sort();
4818
+ const catalogId = review['catalogId'];
4819
+ const descriptor = typeof catalogId === 'string'
4820
+ ? getDeterministicallyDeidentifiedWorkflowTemplate(catalogId)
4821
+ : undefined;
4822
+ if (Object.keys(review).sort().some((key, index) => key !== exactReceiptKeys[index])
4823
+ || Object.keys(review).length !== exactReceiptKeys.length
4824
+ || review['contractVersion'] !== 1
4825
+ || review['kind'] !== 'deterministic-deidentification-template-attestation'
4826
+ || review['method'] !== 'assistant-delivery-redaction-v1'
4827
+ || descriptor === undefined
4828
+ || review['templateDigest'] !== row.template_digest
4829
+ || review['bindingId'] !== row.owner_binding_id
4830
+ || typeof review['principalId'] !== 'string'
4831
+ || typeof receiptBindingVersion !== 'number' || !Number.isSafeInteger(receiptBindingVersion)
4832
+ || receiptBindingVersion < 1
4833
+ || typeof receiptBindingGeneration !== 'number' || !Number.isSafeInteger(receiptBindingGeneration)
4834
+ || receiptBindingGeneration < 1
4835
+ || content.name !== descriptor.name || content.prompt !== descriptor.prompt
4836
+ || content.schedule.kind !== descriptor.schedule.kind
4837
+ || content.schedule.expression !== descriptor.schedule.expression
4838
+ || content.schedule.timezone !== descriptor.schedule.timezone
4839
+ || content.timeoutMs !== descriptor.timeoutMs
4840
+ || JSON.stringify(content.toolCatalogIds) !== JSON.stringify(descriptor.toolCatalogIds)) {
4841
+ throw new DeliveryStoreError('conflict', 'deterministic workflow deidentification receipt is invalid');
4842
+ }
4843
+ attestationDigest = growthObjectDigest({
4844
+ contract: 'assistant-delivery-workflow-template-deidentification/v1',
4845
+ receipt: review,
4846
+ });
4847
+ }
4848
+ else {
4849
+ throw new DeliveryStoreError('conflict', 'workflow template privacy kind is invalid');
4850
+ }
4851
+ const storedTemplate = validateWorkflowAutomationTemplate({
4852
+ templateRef: row.template_ref,
4853
+ templateDigest: row.template_digest,
4854
+ privacyAttestation: row.privacy_kind === 'owner-explicit'
4855
+ ? {
4856
+ kind: 'owner-explicit', limitation: 'deidentification-unproven',
4857
+ attestationId: row.privacy_attestation_id,
4858
+ attestationDigest: row.privacy_attestation_digest,
4859
+ }
4860
+ : {
4861
+ kind: 'deterministic-deidentification', method: 'assistant-delivery-redaction-v1',
4862
+ attestationId: row.privacy_attestation_id,
4863
+ attestationDigest: row.privacy_attestation_digest,
4864
+ },
4865
+ });
4866
+ if (JSON.stringify(storedTemplate) !== JSON.stringify(template)
4867
+ || attestationDigest !== row.privacy_attestation_digest
4868
+ || storedTemplate.privacyAttestation.attestationId !== (row.privacy_kind === 'owner-explicit'
4869
+ ? `workflow-review:${attestationDigest}`
4870
+ : `workflow-deidentification:${attestationDigest}`)) {
4871
+ throw new DeliveryStoreError('receipt-mismatch', 'workflow template attestation is stale');
4872
+ }
4873
+ const contentBinding = this.getBinding(row.owner_binding_id);
4874
+ const resolved = validateResolvedWorkflowAutomationTemplate({
4875
+ contractVersion: 1,
4876
+ template: storedTemplate,
4877
+ ...content,
4878
+ });
4879
+ if (workflowScopeKey(content.scope) !== row.scope_key
4880
+ || content.scope.workspace !== row.workspace || content.scope.preset !== row.preset
4881
+ || content.ownerBindingId !== row.owner_binding_id
4882
+ || contentBinding === undefined
4883
+ || content.principalId !== externalPrincipalId(contentBinding.principal)
4884
+ || workflowAutomationTemplateContentDigest(content) !== row.template_digest) {
4885
+ throw new DeliveryStoreError('receipt-mismatch', 'workflow template content columns do not match');
4886
+ }
4887
+ return Object.freeze({
4888
+ resolved,
4889
+ review: Object.freeze({
4890
+ bindingId: row.owner_binding_id,
4891
+ bindingVersion: review['bindingVersion'],
4892
+ bindingGeneration: review['bindingGeneration'],
4893
+ principalId: review['principalId'],
4894
+ reviewInboxId: row.review_inbox_id,
4895
+ sourceInboxId: row.source_inbox_id,
4896
+ sourceOutboxId: row.source_outbox_id,
4897
+ }),
4898
+ status: row.status,
4899
+ version: row.version,
4900
+ });
4901
+ }
4902
+ requeueCurrentWorkflowTraces() {
4903
+ this.assertOpen();
4904
+ const now = this.now();
4905
+ return this.transaction(() => {
4906
+ this.database.prepare(`
4907
+ UPDATE workflow_trace_outbox
4908
+ SET status = 'delivered', failure_code = 'superseded-revision', updated_at = ?
4909
+ WHERE status IN ('pending', 'retry_wait') AND NOT EXISTS (
4910
+ SELECT 1 FROM workflow_trace_current AS current
4911
+ WHERE current.subject_ref = workflow_trace_outbox.subject_ref
4912
+ AND current.version = workflow_trace_outbox.version
4913
+ )
4914
+ `).run(now);
4915
+ return Number(this.database.prepare(`
4916
+ UPDATE workflow_trace_outbox
4917
+ SET status = 'pending', attempt_count = 0, next_attempt_at = ?, failure_code = NULL, updated_at = ?
4918
+ WHERE (subject_ref, version) IN (
4919
+ SELECT subject_ref, version FROM workflow_trace_current
4920
+ )
4921
+ `).run(now, now).changes);
4922
+ });
4923
+ }
4924
+ enqueuePreferenceProjection(events) {
4925
+ this.assertOpen();
4926
+ const payload = preferenceProjectionPayload(events);
4927
+ return this.transaction(() => this.insertPreferenceProjection(payload));
4928
+ }
4929
+ /**
4930
+ * Persist an Agent reply and its content-free learning projection under one
4931
+ * SQLite commit. A process crash can therefore expose neither row or both,
4932
+ * but never a durable reply that permanently lost its completion signal.
4933
+ */
4934
+ enqueueReplyWithPreferenceProjection(input, eventsForReply) {
4935
+ this.assertOpen();
4936
+ if (typeof eventsForReply !== 'function') {
4937
+ throw new DeliveryStoreError('invalid-intent', 'preference projection builder is invalid');
4938
+ }
4939
+ return this.transaction(() => {
4940
+ const reply = this.enqueue(input);
4941
+ const events = eventsForReply(reply);
4942
+ if (events === undefined)
4943
+ return Object.freeze({ reply });
4944
+ const projection = this.insertPreferenceProjection(preferenceProjectionPayload(events));
4945
+ return Object.freeze({ reply, projection });
4946
+ });
4947
+ }
4948
+ listPendingPreferenceProjections(limitInput = 100, nowInput = this.now()) {
4949
+ this.assertOpen();
4950
+ if (!Number.isSafeInteger(limitInput) || limitInput < 1 || limitInput > 1_000
4951
+ || !Number.isSafeInteger(nowInput) || nowInput < 0) {
4952
+ throw new DeliveryStoreError('conflict', 'preference projection outbox bounds are invalid');
4953
+ }
4954
+ return this.transaction(() => {
4955
+ this.normalizePreferenceProjectionLanes(nowInput);
4956
+ const legacy = this.database.prepare(`
4957
+ SELECT * FROM delivery_preference_projection_outbox
4958
+ WHERE terminal_at IS NULL AND lane_kind <> 'exact'
4959
+ AND status IN ('pending', 'retry_wait')
4960
+ ORDER BY created_at, batch_key
4961
+ LIMIT 1
4962
+ `).get();
4963
+ if (legacy !== undefined) {
4964
+ if (legacy.next_attempt_at > nowInput)
4965
+ return [];
4966
+ return this.validPreferenceProjectionEntries([legacy], nowInput);
4967
+ }
4968
+ const rows = this.database.prepare(`
4969
+ SELECT candidate.*
4970
+ FROM delivery_preference_projection_outbox AS candidate
4971
+ WHERE candidate.terminal_at IS NULL AND candidate.lane_kind = 'exact'
4972
+ AND candidate.status IN ('pending', 'retry_wait')
4973
+ AND candidate.next_attempt_at <= ?
4974
+ AND NOT EXISTS (
4975
+ SELECT 1
4976
+ FROM delivery_preference_projection_outbox AS earlier
4977
+ WHERE earlier.terminal_at IS NULL AND earlier.lane_kind = 'exact'
4978
+ AND earlier.status IN ('pending', 'retry_wait')
4979
+ AND earlier.lane_epoch = candidate.lane_epoch
4980
+ AND earlier.lane_workspace = candidate.lane_workspace
4981
+ AND earlier.lane_preset = candidate.lane_preset
4982
+ AND earlier.lane_principal_record_id = candidate.lane_principal_record_id
4983
+ AND earlier.lane_principal_version = candidate.lane_principal_version
4984
+ AND (
4985
+ earlier.admission_sequence < candidate.admission_sequence
4986
+ OR (
4987
+ earlier.admission_sequence = candidate.admission_sequence
4988
+ AND (
4989
+ earlier.created_at < candidate.created_at
4990
+ OR (earlier.created_at = candidate.created_at
4991
+ AND earlier.batch_key < candidate.batch_key)
4992
+ )
4993
+ )
4994
+ )
4995
+ )
4996
+ ORDER BY candidate.next_attempt_at, candidate.admission_sequence,
4997
+ candidate.created_at, candidate.batch_key
4998
+ LIMIT ?
4999
+ `).all(nowInput, limitInput);
5000
+ return this.validPreferenceProjectionEntries(rows, nowInput);
5001
+ });
5002
+ }
5003
+ /** Earliest retry time among global legacy or exact owner-lane heads. */
5004
+ nextPreferenceProjectionAttemptAt(nowInput = this.now()) {
5005
+ this.assertOpen();
5006
+ if (!Number.isSafeInteger(nowInput) || nowInput < 0) {
5007
+ throw new DeliveryStoreError('conflict', 'preference projection retry clock is invalid');
5008
+ }
5009
+ return this.transaction(() => {
5010
+ this.normalizePreferenceProjectionLanes(nowInput);
5011
+ const legacy = this.database.prepare(`
5012
+ SELECT next_attempt_at FROM delivery_preference_projection_outbox
5013
+ WHERE terminal_at IS NULL AND lane_kind <> 'exact'
5014
+ AND status IN ('pending', 'retry_wait')
5015
+ ORDER BY created_at, batch_key
5016
+ LIMIT 1
5017
+ `).get();
5018
+ if (legacy !== undefined)
5019
+ return legacy.next_attempt_at;
5020
+ const row = this.database.prepare(`
5021
+ SELECT candidate.next_attempt_at
5022
+ FROM delivery_preference_projection_outbox AS candidate
5023
+ WHERE candidate.terminal_at IS NULL AND candidate.lane_kind = 'exact'
5024
+ AND candidate.status IN ('pending', 'retry_wait')
5025
+ AND NOT EXISTS (
5026
+ SELECT 1
5027
+ FROM delivery_preference_projection_outbox AS earlier
5028
+ WHERE earlier.terminal_at IS NULL AND earlier.lane_kind = 'exact'
5029
+ AND earlier.status IN ('pending', 'retry_wait')
5030
+ AND earlier.lane_epoch = candidate.lane_epoch
5031
+ AND earlier.lane_workspace = candidate.lane_workspace
5032
+ AND earlier.lane_preset = candidate.lane_preset
5033
+ AND earlier.lane_principal_record_id = candidate.lane_principal_record_id
5034
+ AND earlier.lane_principal_version = candidate.lane_principal_version
5035
+ AND (
5036
+ earlier.admission_sequence < candidate.admission_sequence
5037
+ OR (
5038
+ earlier.admission_sequence = candidate.admission_sequence
5039
+ AND (
5040
+ earlier.created_at < candidate.created_at
5041
+ OR (earlier.created_at = candidate.created_at
5042
+ AND earlier.batch_key < candidate.batch_key)
5043
+ )
5044
+ )
5045
+ )
5046
+ )
5047
+ ORDER BY candidate.next_attempt_at
5048
+ LIMIT 1
5049
+ `).get();
5050
+ return row?.next_attempt_at;
5051
+ });
5052
+ }
5053
+ /**
5054
+ * A direct control may run only after every earlier projection in its exact
5055
+ * owner lane is terminal. Valid legacy rows are a global upgrade barrier
5056
+ * because their missing cursor makes a narrower comparison impossible.
5057
+ */
5058
+ hasBlockingPreferenceProjectionBefore(input) {
5059
+ this.assertOpen();
5060
+ const workspace = validateBindingText(input.scope.workspace, 'preference workspace', 4_096);
5061
+ const preset = validateBindingText(input.scope.preset, 'preference preset', 200);
5062
+ const principalRecordId = validateBindingText(input.principalLineage.principalRecordId, 'preference principal record id', 500);
5063
+ if (!Number.isSafeInteger(input.principalLineage.principalVersion)
5064
+ || input.principalLineage.principalVersion < 1
5065
+ || !/^[0-9a-f]{32}$/u.test(input.admissionCursor.epoch)
5066
+ || !Number.isSafeInteger(input.admissionCursor.sequence)
5067
+ || input.admissionCursor.sequence < 1) {
5068
+ throw new DeliveryStoreError('conflict', 'preference control barrier authority is invalid');
5069
+ }
5070
+ const now = this.now();
5071
+ return this.transaction(() => {
5072
+ this.normalizePreferenceProjectionLanes(now);
5073
+ const blocker = this.database.prepare(`
5074
+ SELECT 1 AS present
5075
+ FROM delivery_preference_projection_outbox
5076
+ WHERE terminal_at IS NULL AND status IN ('pending', 'retry_wait')
5077
+ AND (
5078
+ lane_kind <> 'exact'
5079
+ OR (
5080
+ lane_epoch = ? AND lane_workspace = ? AND lane_preset = ?
5081
+ AND lane_principal_record_id = ? AND lane_principal_version = ?
5082
+ AND admission_sequence < ?
5083
+ )
5084
+ )
5085
+ LIMIT 1
5086
+ `).get(input.admissionCursor.epoch, workspace, preset, principalRecordId, input.principalLineage.principalVersion, input.admissionCursor.sequence);
5087
+ return blocker !== undefined;
5088
+ });
5089
+ }
5090
+ completePreferenceProjection(input) {
5091
+ this.assertOpen();
5092
+ const changed = this.database.prepare(`
5093
+ DELETE FROM delivery_preference_projection_outbox
5094
+ WHERE batch_key = ? AND payload_digest = ? AND terminal_at IS NULL
5095
+ `).run(input.batchKey, input.payloadDigest);
5096
+ if (changed.changes === 1)
5097
+ return;
5098
+ const current = this.database.prepare(`
5099
+ SELECT payload_digest, terminal_at
5100
+ FROM delivery_preference_projection_outbox WHERE batch_key = ?
5101
+ `).get(input.batchKey);
5102
+ // Another Host can append the same idempotent batch and delete it first.
5103
+ // Absence is therefore the peer-completed terminal, not a local failure.
5104
+ if (current === undefined)
5105
+ return;
5106
+ if (current.payload_digest !== input.payloadDigest) {
5107
+ throw new DeliveryStoreError('receipt-mismatch', 'preference projection completion changed identity');
5108
+ }
5109
+ // A concurrent owner handoff terminally quarantines this exact batch. Its
5110
+ // late completion is an ACK-ignore terminal, never a reason to revive or
5111
+ // retry evidence from the retired lineage.
5112
+ if (current.terminal_at !== null)
5113
+ return;
5114
+ throw new DeliveryStoreError('version-conflict', 'preference projection completion lost its active batch');
5115
+ }
5116
+ deferPreferenceProjection(input) {
5117
+ this.assertOpen();
5118
+ const failureCode = validateBindingText(input.failureCode, 'preference projection failureCode', 64);
5119
+ if (!Number.isSafeInteger(input.now) || !Number.isSafeInteger(input.retryAt)
5120
+ || input.retryAt <= input.now) {
5121
+ throw new DeliveryStoreError('conflict', 'preference projection retry time is invalid');
5122
+ }
5123
+ const changed = this.database.prepare(`
5124
+ UPDATE delivery_preference_projection_outbox
5125
+ SET status = 'retry_wait', attempt_count = attempt_count + 1,
5126
+ next_attempt_at = ?, failure_code = ?, updated_at = ?
5127
+ WHERE batch_key = ? AND payload_digest = ? AND terminal_at IS NULL
5128
+ `).run(input.retryAt, failureCode, input.now, input.batchKey, input.payloadDigest);
5129
+ if (changed.changes === 1)
5130
+ return;
5131
+ const current = this.database.prepare(`
5132
+ SELECT payload_digest, terminal_at
5133
+ FROM delivery_preference_projection_outbox WHERE batch_key = ?
5134
+ `).get(input.batchKey);
5135
+ // If a peer received the authoritative receipt and removed this row while
5136
+ // our append was failing, its exact downstream commit is already enough.
5137
+ if (current === undefined)
5138
+ return;
5139
+ if (current.payload_digest !== input.payloadDigest) {
5140
+ throw new DeliveryStoreError('receipt-mismatch', 'preference projection defer changed identity');
5141
+ }
5142
+ if (current.terminal_at !== null)
5143
+ return;
5144
+ throw new DeliveryStoreError('version-conflict', 'preference projection defer lost its active batch');
5145
+ }
5146
+ /**
5147
+ * Revalidate one previously-read batch against the live Delivery owner.
5148
+ * Stale, legacy, poison, already-terminal, and peer-completed rows are
5149
+ * acknowledged without invoking a downstream sink.
5150
+ */
5151
+ preferenceProjectionHasCurrentOwner(input) {
5152
+ this.assertOpen();
5153
+ return this.transaction(() => this.currentPreferenceProjectionInTransaction(input) !== undefined);
5154
+ }
5155
+ /**
5156
+ * Linearizable projection for a process-local synchronous Preference writer.
5157
+ * Delivery's BEGIN IMMEDIATE remains held while Preference commits, giving
5158
+ * all cooperating processes the fixed Delivery -> Preference lock order.
5159
+ * A handoff therefore linearizes wholly before the callback (which is then
5160
+ * skipped) or wholly after the Preference commit and Delivery ACK.
5161
+ */
5162
+ projectPreferenceProjectionUnderOwnerFence(input, project) {
5163
+ this.assertOpen();
5164
+ if (typeof project !== 'function') {
5165
+ throw new DeliveryStoreError('invalid-intent', 'preference projection writer is invalid');
5166
+ }
5167
+ return this.transaction(() => {
5168
+ const row = this.database.prepare(`
5169
+ SELECT * FROM delivery_preference_projection_outbox WHERE batch_key = ?
5170
+ `).get(input.batchKey);
5171
+ if (row === undefined)
5172
+ return 'missing';
5173
+ if (row.payload_digest !== input.payloadDigest) {
5174
+ throw new DeliveryStoreError('receipt-mismatch', 'preference projection fence changed identity');
5175
+ }
5176
+ const entry = this.currentPreferenceProjectionInTransaction(input, row);
5177
+ if (entry === undefined)
5178
+ return 'ignored';
5179
+ const result = project(entry);
5180
+ if (typeof result === 'object' && result !== null
5181
+ && typeof result.then === 'function') {
5182
+ throw new DeliveryStoreError('invalid-intent', 'preference projection fenced writer must complete synchronously');
5183
+ }
5184
+ const deleted = this.database.prepare(`
5185
+ DELETE FROM delivery_preference_projection_outbox
5186
+ WHERE batch_key = ? AND payload_digest = ? AND terminal_at IS NULL
5187
+ `).run(entry.batchKey, entry.payloadDigest);
5188
+ if (deleted.changes !== 1) {
5189
+ throw new DeliveryStoreError('version-conflict', 'preference projection fenced ACK lost its batch');
5190
+ }
5191
+ return 'completed';
5192
+ });
5193
+ }
5194
+ requeuePreferenceProjections() {
5195
+ this.assertOpen();
5196
+ const now = this.now();
5197
+ return Number(this.database.prepare(`
5198
+ UPDATE delivery_preference_projection_outbox
5199
+ SET status = 'pending', next_attempt_at = ?, failure_code = NULL, updated_at = ?
5200
+ WHERE terminal_at IS NULL
5201
+ `).run(now, now).changes);
5202
+ }
5203
+ normalizePreferenceProjectionLanes(now) {
5204
+ const rows = this.database.prepare(`
5205
+ SELECT * FROM delivery_preference_projection_outbox
5206
+ WHERE terminal_at IS NULL
5207
+ ORDER BY created_at, batch_key
5208
+ `).all();
5209
+ for (const row of rows) {
5210
+ try {
5211
+ const entry = preferenceProjectionOutboxEntry(row);
5212
+ if (row.lane_kind !== 'unclassified')
5213
+ continue;
5214
+ const changed = entry.lane === undefined
5215
+ ? this.database.prepare(`
5216
+ UPDATE delivery_preference_projection_outbox
5217
+ SET lane_kind = 'legacy', lane_epoch = NULL, lane_workspace = NULL,
5218
+ lane_preset = NULL, lane_principal_record_id = NULL,
5219
+ lane_principal_version = NULL, admission_sequence = NULL,
5220
+ updated_at = ?
5221
+ WHERE batch_key = ? AND payload_digest = ?
5222
+ AND terminal_at IS NULL AND lane_kind = 'unclassified'
5223
+ `).run(now, entry.batchKey, entry.payloadDigest)
5224
+ : this.database.prepare(`
5225
+ UPDATE delivery_preference_projection_outbox
5226
+ SET lane_kind = 'exact', lane_epoch = ?, lane_workspace = ?,
5227
+ lane_preset = ?, lane_principal_record_id = ?,
5228
+ lane_principal_version = ?, admission_sequence = ?, updated_at = ?
5229
+ WHERE batch_key = ? AND payload_digest = ?
5230
+ AND terminal_at IS NULL AND lane_kind = 'unclassified'
5231
+ `).run(entry.lane.epoch, entry.lane.workspace, entry.lane.preset, entry.lane.principalRecordId, entry.lane.principalVersion, entry.lane.admissionSequence, now, entry.batchKey, entry.payloadDigest);
5232
+ if (changed.changes !== 1) {
5233
+ throw new DeliveryStoreError('version-conflict', 'preference projection lane normalization lost its exact batch');
5234
+ }
5235
+ }
5236
+ catch {
5237
+ this.database.prepare(`
5238
+ UPDATE delivery_preference_projection_outbox
5239
+ SET terminal_at = ?, failure_code = 'projection-poison-row',
5240
+ attempt_count = attempt_count + 1, next_attempt_at = 9007199254740991,
5241
+ updated_at = ?
5242
+ WHERE batch_key = ? AND terminal_at IS NULL
5243
+ `).run(now, now, row.batch_key);
5244
+ }
5245
+ }
5246
+ }
5247
+ currentPreferenceProjectionInTransaction(input, selected) {
5248
+ const row = selected ?? this.database.prepare(`
5249
+ SELECT * FROM delivery_preference_projection_outbox WHERE batch_key = ?
5250
+ `).get(input.batchKey);
5251
+ if (row === undefined)
5252
+ return undefined;
5253
+ if (row.payload_digest !== input.payloadDigest) {
5254
+ throw new DeliveryStoreError('receipt-mismatch', 'preference projection owner fence changed identity');
5255
+ }
5256
+ if (row.terminal_at !== null)
5257
+ return undefined;
5258
+ let entry;
5259
+ try {
5260
+ entry = preferenceProjectionOutboxEntry(row);
5261
+ }
5262
+ catch {
5263
+ this.terminalizePreferenceProjectionInTransaction(row.batch_key, 'projection-poison-row');
5264
+ return undefined;
5265
+ }
5266
+ const owners = this.database.prepare(`
5267
+ SELECT id, principal_json, role, status, linked_to_id, created_at, updated_at, version
5268
+ FROM delivery_principals WHERE role = 'owner' AND status = 'active'
5269
+ ORDER BY id LIMIT 2
5270
+ `).all();
5271
+ let currentPrincipalId;
5272
+ try {
5273
+ currentPrincipalId = owners.length === 1
5274
+ ? externalPrincipalId(JSON.parse(owners[0].principal_json))
5275
+ : undefined;
5276
+ }
5277
+ catch {
5278
+ currentPrincipalId = undefined;
5279
+ }
5280
+ const owner = owners.length === 1 ? owners[0] : undefined;
5281
+ if (row.lane_kind !== 'exact' || entry.lane === undefined || owner === undefined
5282
+ || entry.lane.principalRecordId !== owner.id
5283
+ || entry.lane.principalVersion !== owner.version
5284
+ || entry.events.some(event => event.principalId !== currentPrincipalId)) {
5285
+ this.terminalizePreferenceProjectionInTransaction(row.batch_key, 'owner-lineage-retired');
5286
+ return undefined;
5287
+ }
5288
+ return entry;
5289
+ }
5290
+ terminalizePreferenceProjectionInTransaction(batchKey, failureCode) {
5291
+ const now = this.now();
5292
+ this.database.prepare(`
5293
+ UPDATE delivery_preference_projection_outbox
5294
+ SET terminal_at = ?, failure_code = ?, next_attempt_at = 9007199254740991,
5295
+ updated_at = ?
5296
+ WHERE batch_key = ? AND terminal_at IS NULL
5297
+ `).run(now, failureCode, now, batchKey);
5298
+ }
5299
+ validPreferenceProjectionEntries(rows, now) {
5300
+ const result = [];
5301
+ for (const row of rows) {
5302
+ try {
5303
+ result.push(preferenceProjectionOutboxEntry(row));
5304
+ }
5305
+ catch {
5306
+ this.database.prepare(`
5307
+ UPDATE delivery_preference_projection_outbox
5308
+ SET terminal_at = ?, failure_code = 'projection-poison-row',
5309
+ attempt_count = attempt_count + 1, next_attempt_at = 9007199254740991,
5310
+ updated_at = ?
5311
+ WHERE batch_key = ? AND terminal_at IS NULL
5312
+ `).run(now, now, row.batch_key);
5313
+ }
5314
+ }
5315
+ return result;
5316
+ }
5317
+ insertPreferenceProjection(payload) {
5318
+ const existing = this.database.prepare(`
5319
+ SELECT payload_digest, events_json
5320
+ FROM delivery_preference_projection_outbox WHERE batch_key = ?
5321
+ `).get(payload.batchKey);
5322
+ if (existing !== undefined) {
5323
+ if (existing.payload_digest !== payload.digest || existing.events_json !== payload.json) {
5324
+ throw new DeliveryStoreError('idempotency-conflict', 'preference projection identity was reused with different events');
5325
+ }
5326
+ return Object.freeze({
5327
+ batchKey: payload.batchKey,
5328
+ payloadDigest: payload.digest,
5329
+ replayed: true,
5330
+ });
5331
+ }
5332
+ const now = this.now();
5333
+ this.database.prepare(`
5334
+ INSERT INTO delivery_preference_projection_outbox(
5335
+ batch_key, payload_digest, events_json, status, attempt_count,
5336
+ next_attempt_at, failure_code, lane_kind, lane_epoch, lane_workspace,
5337
+ lane_preset, lane_principal_record_id, lane_principal_version,
5338
+ admission_sequence, terminal_at, created_at, updated_at
5339
+ ) VALUES (?, ?, ?, 'pending', 0, ?, NULL, ?, ?, ?, ?, ?, ?, ?, NULL, ?, ?)
5340
+ `).run(payload.batchKey, payload.digest, payload.json, now, payload.lane === undefined ? 'legacy' : 'exact', payload.lane?.epoch ?? null, payload.lane?.workspace ?? null, payload.lane?.preset ?? null, payload.lane?.principalRecordId ?? null, payload.lane?.principalVersion ?? null, payload.lane?.admissionSequence ?? null, now, now);
5341
+ return Object.freeze({
5342
+ batchKey: payload.batchKey,
5343
+ payloadDigest: payload.digest,
5344
+ replayed: false,
5345
+ });
5346
+ }
5347
+ listPendingWorkflowTraceRevisions(limitInput = 100, nowInput = this.now()) {
5348
+ this.assertOpen();
5349
+ if (!Number.isSafeInteger(limitInput) || limitInput < 1 || limitInput > 1_000
5350
+ || !Number.isSafeInteger(nowInput) || nowInput < 0) {
5351
+ throw new DeliveryStoreError('conflict', 'workflow trace outbox bounds are invalid');
5352
+ }
5353
+ const rows = this.database.prepare(`
5354
+ SELECT revision.*, outbox.status, outbox.attempt_count, outbox.next_attempt_at,
5355
+ outbox.failure_code, outbox.updated_at
5356
+ FROM workflow_trace_outbox AS outbox
5357
+ JOIN workflow_trace_revisions AS revision
5358
+ ON revision.subject_ref = outbox.subject_ref AND revision.version = outbox.version
5359
+ JOIN workflow_trace_current AS current
5360
+ ON current.subject_ref = outbox.subject_ref AND current.version = outbox.version
5361
+ WHERE outbox.status IN ('pending', 'retry_wait') AND outbox.next_attempt_at <= ?
5362
+ ORDER BY outbox.next_attempt_at, outbox.updated_at, outbox.subject_ref, outbox.version
5363
+ LIMIT ?
5364
+ `).all(nowInput, limitInput);
5365
+ const result = [];
5366
+ for (const row of rows) {
5367
+ try {
5368
+ result.push(workflowTraceOutboxEntry(row));
5369
+ }
5370
+ catch {
5371
+ this.deferWorkflowTraceRevision({
5372
+ subjectRef: row.subject_ref,
5373
+ version: row.version,
5374
+ digest: row.digest,
5375
+ now: nowInput,
5376
+ retryAt: nowInput + 86_400_000,
5377
+ failureCode: 'trace-poison-row',
5378
+ });
5379
+ }
5380
+ }
5381
+ return result;
5382
+ }
5383
+ completeWorkflowTraceRevision(input) {
5384
+ this.assertOpen();
5385
+ const now = input.now ?? this.now();
5386
+ const changed = this.database.prepare(`
5387
+ UPDATE workflow_trace_outbox
5388
+ SET status = 'delivered', failure_code = NULL, next_attempt_at = ?, updated_at = ?
5389
+ WHERE subject_ref = ? AND version = ? AND status IN ('pending', 'retry_wait')
5390
+ AND EXISTS (
5391
+ SELECT 1 FROM workflow_trace_revisions AS revision
5392
+ WHERE revision.subject_ref = workflow_trace_outbox.subject_ref
5393
+ AND revision.version = workflow_trace_outbox.version AND revision.digest = ?
5394
+ )
5395
+ `).run(now, now, input.subjectRef, input.version, input.digest);
5396
+ if (changed.changes !== 1) {
5397
+ const current = this.database.prepare(`
5398
+ SELECT outbox.status, revision.digest FROM workflow_trace_outbox AS outbox
5399
+ JOIN workflow_trace_revisions AS revision
5400
+ ON revision.subject_ref = outbox.subject_ref AND revision.version = outbox.version
5401
+ WHERE outbox.subject_ref = ? AND outbox.version = ?
5402
+ `).get(input.subjectRef, input.version);
5403
+ if (current?.status !== 'delivered' || current.digest !== input.digest) {
5404
+ throw new DeliveryStoreError('version-conflict', 'workflow trace completion lost its exact revision');
5405
+ }
5406
+ }
5407
+ }
5408
+ deferWorkflowTraceRevision(input) {
5409
+ this.assertOpen();
5410
+ const failureCode = validateBindingText(input.failureCode, 'workflow trace failureCode', 64);
5411
+ if (!Number.isSafeInteger(input.now) || !Number.isSafeInteger(input.retryAt)
5412
+ || input.retryAt <= input.now)
5413
+ throw new DeliveryStoreError('conflict', 'workflow trace retry time is invalid');
5414
+ const changed = this.database.prepare(`
5415
+ UPDATE workflow_trace_outbox
5416
+ SET status = 'retry_wait', attempt_count = attempt_count + 1,
5417
+ next_attempt_at = ?, failure_code = ?, updated_at = ?
5418
+ WHERE subject_ref = ? AND version = ? AND status IN ('pending', 'retry_wait')
5419
+ AND EXISTS (
5420
+ SELECT 1 FROM workflow_trace_revisions AS revision
5421
+ WHERE revision.subject_ref = workflow_trace_outbox.subject_ref
5422
+ AND revision.version = workflow_trace_outbox.version AND revision.digest = ?
5423
+ )
5424
+ `).run(input.retryAt, failureCode, input.now, input.subjectRef, input.version, input.digest);
5425
+ if (changed.changes !== 1) {
5426
+ throw new DeliveryStoreError('version-conflict', 'workflow trace defer lost its exact revision');
5427
+ }
5428
+ }
5429
+ insertWorkflowTraceRevisionInTransaction(input) {
5430
+ const revision = validateWorkflowTraceRevision(input);
5431
+ const source = this.workflowTraceSourceAttestation();
5432
+ if (revision.source.sourceId !== source.sourceId
5433
+ || revision.source.generation !== source.generation
5434
+ || revision.source.authorityDigest !== source.authorityDigest) {
5435
+ throw new DeliveryStoreError('unauthorized-principal', 'workflow trace source authority changed');
5436
+ }
5437
+ const existingRow = this.database.prepare(`
5438
+ SELECT * FROM workflow_trace_revisions WHERE subject_ref = ? AND version = ?
5439
+ `).get(revision.subjectRef, revision.version);
5440
+ if (existingRow !== undefined) {
5441
+ const existing = workflowTraceRevision(existingRow);
5442
+ if (existing.digest !== revision.digest
5443
+ || JSON.stringify(existing) !== JSON.stringify(revision)) {
5444
+ throw new DeliveryStoreError('idempotency-conflict', 'workflow trace version was reused with different content');
5445
+ }
5446
+ const current = this.database.prepare(`
5447
+ SELECT version, digest FROM workflow_trace_current WHERE subject_ref = ?
5448
+ `).get(revision.subjectRef);
5449
+ if (current === undefined || current.version < revision.version
5450
+ || (current.version === revision.version && current.digest !== revision.digest)) {
5451
+ throw new DeliveryStoreError('conflict', 'workflow trace current projection is inconsistent');
5452
+ }
5453
+ return Object.freeze({ revision: existing, replayed: true });
5454
+ }
5455
+ const current = this.database.prepare(`
5456
+ SELECT version, digest FROM workflow_trace_current WHERE subject_ref = ?
5457
+ `).get(revision.subjectRef);
5458
+ if (current !== undefined && revision.version <= current.version) {
5459
+ throw new DeliveryStoreError('version-conflict', 'workflow trace version is lower than the current projection');
5460
+ }
5461
+ const now = this.now();
5462
+ this.database.prepare(`
5463
+ INSERT INTO workflow_trace_revisions(
5464
+ subject_ref, version, source_generation, source_authority_digest, scope_key,
5465
+ workspace, preset, disposition, digest, payload_json, created_at
5466
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
5467
+ `).run(revision.subjectRef, revision.version, revision.source.generation, revision.source.authorityDigest, workflowScopeKey(revision.scope), revision.scope.workspace, revision.scope.preset, revision.disposition, revision.digest, JSON.stringify(revision), now);
5468
+ this.database.prepare(`
5469
+ INSERT INTO workflow_trace_current(
5470
+ subject_ref, version, digest, disposition, payload_json, updated_at
5471
+ ) VALUES (?, ?, ?, ?, ?, ?)
5472
+ ON CONFLICT(subject_ref) DO UPDATE SET
5473
+ version = excluded.version,
5474
+ digest = excluded.digest,
5475
+ disposition = excluded.disposition,
5476
+ payload_json = excluded.payload_json,
5477
+ updated_at = excluded.updated_at
5478
+ WHERE excluded.version > workflow_trace_current.version
5479
+ `).run(revision.subjectRef, revision.version, revision.digest, revision.disposition, JSON.stringify(revision), now);
5480
+ this.database.prepare(`
5481
+ INSERT INTO workflow_trace_outbox(
5482
+ subject_ref, version, status, attempt_count, next_attempt_at,
5483
+ failure_code, created_at, updated_at
5484
+ ) VALUES (?, ?, 'pending', 0, ?, NULL, ?, ?)
5485
+ `).run(revision.subjectRef, revision.version, now, now, now);
5486
+ return Object.freeze({ revision, replayed: false });
5487
+ }
5488
+ revokeCurrentWorkflowTemplateInTransaction(current, now) {
5489
+ const template = current?.disposition === 'upsert' ? current.evidence?.template : undefined;
5490
+ if (template === undefined)
5491
+ return;
5492
+ const row = this.database.prepare(`
5493
+ SELECT template_digest, status FROM workflow_template_registry WHERE template_ref = ?
5494
+ `).get(template.templateRef);
5495
+ if (row === undefined)
5496
+ return;
5497
+ if (row.template_digest !== template.templateDigest) {
5498
+ throw new DeliveryStoreError('receipt-mismatch', 'current workflow template identity is corrupt');
5499
+ }
5500
+ if (row.status === 'revoked')
5501
+ return;
5502
+ const changed = this.database.prepare(`
5503
+ UPDATE workflow_template_registry
5504
+ SET status = 'revoked', updated_at = ?, version = version + 1
5505
+ WHERE template_ref = ? AND template_digest = ? AND status = 'active'
5506
+ `).run(now, template.templateRef, template.templateDigest);
5507
+ if (changed.changes !== 1) {
5508
+ throw new DeliveryStoreError('version-conflict', 'workflow template revocation lost its exact version');
5509
+ }
5510
+ }
2629
5511
  close() {
2630
5512
  if (this.closed)
2631
5513
  return;