@pegma/support-desk-application 0.1.0 → 0.2.0

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 (86) hide show
  1. package/README.md +228 -7
  2. package/dist/assistant-turn-capacity.d.ts +13 -0
  3. package/dist/assistant-turn-capacity.d.ts.map +1 -0
  4. package/dist/assistant-turn-capacity.js +111 -0
  5. package/dist/assistant-turn-capacity.js.map +1 -0
  6. package/dist/assistant-turn-records.d.ts +68 -0
  7. package/dist/assistant-turn-records.d.ts.map +1 -0
  8. package/dist/assistant-turn-records.js +418 -0
  9. package/dist/assistant-turn-records.js.map +1 -0
  10. package/dist/assistant-turn-types.d.ts +133 -0
  11. package/dist/assistant-turn-types.d.ts.map +1 -0
  12. package/dist/assistant-turn-types.js +2 -0
  13. package/dist/assistant-turn-types.js.map +1 -0
  14. package/dist/assistant-turns.d.ts +42 -0
  15. package/dist/assistant-turns.d.ts.map +1 -0
  16. package/dist/assistant-turns.js +640 -0
  17. package/dist/assistant-turns.js.map +1 -0
  18. package/dist/engineering-evidence.d.ts +20 -0
  19. package/dist/engineering-evidence.d.ts.map +1 -0
  20. package/dist/engineering-evidence.js +899 -0
  21. package/dist/engineering-evidence.js.map +1 -0
  22. package/dist/engineering-followup-internal.d.ts +45 -0
  23. package/dist/engineering-followup-internal.d.ts.map +1 -0
  24. package/dist/engineering-followup-internal.js +2 -0
  25. package/dist/engineering-followup-internal.js.map +1 -0
  26. package/dist/engineering-followup-records.d.ts +209 -0
  27. package/dist/engineering-followup-records.d.ts.map +1 -0
  28. package/dist/engineering-followup-records.js +1434 -0
  29. package/dist/engineering-followup-records.js.map +1 -0
  30. package/dist/engineering-followup-source.d.ts +168 -0
  31. package/dist/engineering-followup-source.d.ts.map +1 -0
  32. package/dist/engineering-followup-source.js +1144 -0
  33. package/dist/engineering-followup-source.js.map +1 -0
  34. package/dist/engineering-followup-types.d.ts +364 -0
  35. package/dist/engineering-followup-types.d.ts.map +1 -0
  36. package/dist/engineering-followup-types.js +2 -0
  37. package/dist/engineering-followup-types.js.map +1 -0
  38. package/dist/engineering-followup-work.d.ts +70 -0
  39. package/dist/engineering-followup-work.d.ts.map +1 -0
  40. package/dist/engineering-followup-work.js +1153 -0
  41. package/dist/engineering-followup-work.js.map +1 -0
  42. package/dist/engineering-internal.d.ts +38 -0
  43. package/dist/engineering-internal.d.ts.map +1 -0
  44. package/dist/engineering-internal.js +11 -0
  45. package/dist/engineering-internal.js.map +1 -0
  46. package/dist/engineering-policy.d.ts +47 -0
  47. package/dist/engineering-policy.d.ts.map +1 -0
  48. package/dist/engineering-policy.js +579 -0
  49. package/dist/engineering-policy.js.map +1 -0
  50. package/dist/engineering-reconciliation.d.ts +6 -0
  51. package/dist/engineering-reconciliation.d.ts.map +1 -0
  52. package/dist/engineering-reconciliation.js +464 -0
  53. package/dist/engineering-reconciliation.js.map +1 -0
  54. package/dist/engineering-records.d.ts +227 -0
  55. package/dist/engineering-records.d.ts.map +1 -0
  56. package/dist/engineering-records.js +1272 -0
  57. package/dist/engineering-records.js.map +1 -0
  58. package/dist/engineering-types.d.ts +505 -0
  59. package/dist/engineering-types.d.ts.map +1 -0
  60. package/dist/engineering-types.js +2 -0
  61. package/dist/engineering-types.js.map +1 -0
  62. package/dist/engineering-work.d.ts +32 -0
  63. package/dist/engineering-work.d.ts.map +1 -0
  64. package/dist/engineering-work.js +1757 -0
  65. package/dist/engineering-work.js.map +1 -0
  66. package/dist/index.d.ts +609 -14
  67. package/dist/index.d.ts.map +1 -1
  68. package/dist/index.js +3568 -204
  69. package/dist/index.js.map +1 -1
  70. package/dist/resolution-learning-records.d.ts +154 -0
  71. package/dist/resolution-learning-records.d.ts.map +1 -0
  72. package/dist/resolution-learning-records.js +657 -0
  73. package/dist/resolution-learning-records.js.map +1 -0
  74. package/dist/resolution-learning-types.d.ts +356 -0
  75. package/dist/resolution-learning-types.d.ts.map +1 -0
  76. package/dist/resolution-learning-types.js +2 -0
  77. package/dist/resolution-learning-types.js.map +1 -0
  78. package/dist/resolution-learning-worker.d.ts +21 -0
  79. package/dist/resolution-learning-worker.d.ts.map +1 -0
  80. package/dist/resolution-learning-worker.js +1027 -0
  81. package/dist/resolution-learning-worker.js.map +1 -0
  82. package/dist/resolution-learning.d.ts +38 -0
  83. package/dist/resolution-learning.d.ts.map +1 -0
  84. package/dist/resolution-learning.js +860 -0
  85. package/dist/resolution-learning.js.map +1 -0
  86. package/package.json +8 -8
package/dist/index.d.ts CHANGED
@@ -3,7 +3,29 @@ import { type AccessContext } from "@pegma/authorization-core";
3
3
  import { type MailJob } from "@pegma/mail";
4
4
  import type { Clock, IsoTimestamp, Logger, PrincipalId } from "@pegma/spine";
5
5
  import { type Store } from "@pegma/storage-core";
6
- import type { MessageId, RequesterAssociation, Ticket, TicketChannel, TicketId, TicketMessage, TicketPriority, TicketStatus } from "@pegma/support-desk-contracts";
6
+ import type { InboundSenderAuthentication, MessageFormat, MessageId, RequesterAssociation, Ticket, TicketChannel, TicketId, TicketMessage, TicketPriority, TicketStatus, TicketHandling, TicketHandlingState } from "@pegma/support-desk-contracts";
7
+ import type { AssistantTurnApplication, AssistantTurnOptions } from "./assistant-turn-types.js";
8
+ import { type AssistantTurnRecord } from "./assistant-turn-records.js";
9
+ import type { ResolutionLearningApplication, ResolutionLearningOptions, ResolutionLearningSourceResult } from "./resolution-learning-types.js";
10
+ import type { EngineeringApplication, EngineeringOptions, EngineeringPolicyApplication } from "./engineering-types.js";
11
+ import { type EngineeringSourceRecord, type EngineeringWorkRecord, type EngineeringReceiptRecord } from "./engineering-records.js";
12
+ import type { EngineeringFollowupApplication, EngineeringFollowupOptions } from "./engineering-followup-types.js";
13
+ import { type EngineeringFollowupRecord } from "./engineering-followup-records.js";
14
+ import { type ResolutionLearningReviewRecord, type ResolutionLearningSourceRecord, type ResolutionLearningWorkRecord } from "./resolution-learning-records.js";
15
+ export type * from "./resolution-learning-types.js";
16
+ export type { ResolutionLearningReviewRecord, ResolutionLearningSourceRecord, ResolutionLearningWorkRecord, } from "./resolution-learning-records.js";
17
+ export { resolutionLearningDeliveryBinding, resolutionLearningManifestHash, } from "./resolution-learning-worker.js";
18
+ export type * from "./assistant-turn-types.js";
19
+ export type { AssistantTurnRecord } from "./assistant-turn-records.js";
20
+ export type { InboundSenderAuthentication };
21
+ export type * from "./engineering-types.js";
22
+ export type * from "./engineering-followup-types.js";
23
+ export type { EngineeringFollowupHeadRecord, EngineeringFollowupWorkRecord, EngineeringFollowupReceiptRecord, EngineeringFollowupRecord, EngineeringFollowupPreparedBundle, EngineeringFollowupRetestSummary, } from "./engineering-followup-records.js";
24
+ export { engineeringFollowupModelManifestHash, engineeringFollowupContextHash, engineeringFollowupPublicFactsHash, engineeringFollowupChecksBinding, engineeringFollowupRetestRequestId, engineeringFollowupRetestProductBinding, engineeringFollowupRetestRequestHash, engineeringFollowupRetestSummary, engineeringFollowupModelAttemptCount, } from "./engineering-followup-records.js";
25
+ export type { EngineeringSourceRecord, EngineeringWorkRecord, EngineeringReceiptRecord, EngineeringPreparedProposal, } from "./engineering-records.js";
26
+ export { engineeringPolicyRecords, engineeringPolicyAudit, engineeringPolicyAuditActions, createEngineeringPolicyServices, } from "./engineering-policy.js";
27
+ export type { EngineeringPolicyOptions, EngineeringPolicyRecord, } from "./engineering-policy.js";
28
+ export { engineeringModelManifestHash, engineeringRoutingBinding, engineeringExportChecksBinding, engineeringRoutingBindingHash, engineeringStaffSelectionHash, } from "./engineering-work.js";
7
29
  export declare const supportPermissions: Readonly<{
8
30
  readonly create: "support.ticket.create";
9
31
  readonly readOwn: "support.ticket.read.own";
@@ -14,6 +36,32 @@ export declare const supportPermissions: Readonly<{
14
36
  readonly assign: "support.ticket.assign";
15
37
  readonly manage: "support.ticket.manage";
16
38
  readonly auditRead: "support.audit.read";
39
+ readonly assistantStart: "support.ticket.assistant.start";
40
+ readonly assistantRead: "support.ticket.assistant.read";
41
+ readonly assistantReply: "support.ticket.assistant.reply";
42
+ readonly assistantEscalate: "support.ticket.assistant.escalate";
43
+ readonly requestPerson: "support.ticket.request_person.own";
44
+ readonly takeOver: "support.ticket.take_over";
45
+ readonly confirmSuccess: "support.ticket.confirm_success.own";
46
+ readonly learningRead: "support.learning.read";
47
+ readonly learningEvidenceSubmit: "support.learning.evidence.submit";
48
+ readonly learningProcess: "support.learning.process";
49
+ readonly learningReview: "support.learning.review";
50
+ readonly learningWithdraw: "support.learning.withdraw";
51
+ readonly engineeringRead: "support.engineering.read";
52
+ readonly engineeringEvidenceSubmit: "support.engineering.evidence.submit";
53
+ readonly engineeringProcess: "support.engineering.process";
54
+ readonly engineeringReview: "support.engineering.review";
55
+ readonly engineeringWithdraw: "support.engineering.withdraw";
56
+ readonly engineeringRestrictedRead: "support.engineering.restricted.read";
57
+ readonly engineeringSecurityReview: "support.engineering.security.review";
58
+ readonly engineeringReconcile: "support.engineering.reconcile";
59
+ readonly engineeringPolicyRead: "support.engineering.policy.read";
60
+ readonly engineeringPolicyManage: "support.engineering.policy.manage";
61
+ readonly engineeringFollowupRead: "support.engineering.followup.read";
62
+ readonly engineeringFollowupProcess: "support.engineering.followup.process";
63
+ readonly engineeringFollowupReview: "support.engineering.followup.review";
64
+ readonly engineeringFollowupEvidenceSubmit: "support.engineering.followup.evidence.submit";
17
65
  }>;
18
66
  export interface SupportDeskLimits {
19
67
  readonly maxSubjectCharacters: number;
@@ -38,10 +86,37 @@ export declare class SupportDeskNotFoundError extends Error {
38
86
  export declare class SupportDeskConflictError extends Error {
39
87
  constructor(message?: string);
40
88
  }
89
+ /** Content-safe collision hint, available only after staff authorization. */
90
+ export declare class SupportDeskStaleRevisionError extends SupportDeskConflictError {
91
+ readonly currentRevision: number;
92
+ readonly updatedAt: IsoTimestamp;
93
+ constructor(currentRevision: number, updatedAt: IsoTimestamp);
94
+ }
95
+ export interface ResponseTargets {
96
+ readonly firstResponseDueAt?: IsoTimestamp;
97
+ readonly resolutionDueAt?: IsoTimestamp;
98
+ }
99
+ /** Content-free facts; hosts own calendars, presence, and target calculation. */
100
+ export interface ResponseTargetContext extends ResponseTargets {
101
+ readonly reason: "created" | "customer_replied" | "priority_changed";
102
+ readonly ticketId: TicketId;
103
+ readonly revision: number;
104
+ readonly priority: TicketPriority;
105
+ readonly category?: string;
106
+ readonly channel: TicketChannel;
107
+ readonly createdAt: IsoTimestamp;
108
+ readonly occurredAt: IsoTimestamp;
109
+ readonly firstStaffRespondedAt?: IsoTimestamp;
110
+ readonly resolvedAt?: IsoTimestamp;
111
+ }
112
+ export interface ResponseTargetPolicy {
113
+ /** Complete replacement of due timestamps. Must be safe to retry on conflicts. */
114
+ computeTargets(context: ResponseTargetContext): ResponseTargets | Promise<ResponseTargets>;
115
+ }
41
116
  export declare class SupportDeskLimitError extends Error {
42
- readonly field: "subject" | "body" | "customer_tickets" | "ticket_messages" | "ticket_partition";
117
+ readonly field: "subject" | "body" | "handoff_summary" | "serialized_record" | "customer_tickets" | "engineering_policy_records" | "engineering_policy_repositories" | "ticket_messages" | "ticket_partition";
43
118
  readonly maximum: number;
44
- constructor(field: "subject" | "body" | "customer_tickets" | "ticket_messages" | "ticket_partition", maximum: number);
119
+ constructor(field: "subject" | "body" | "handoff_summary" | "serialized_record" | "customer_tickets" | "engineering_policy_records" | "engineering_policy_repositories" | "ticket_messages" | "ticket_partition", maximum: number);
45
120
  }
46
121
  /**
47
122
  * Staff queue online scan hit a configured materialization budget before the
@@ -67,6 +142,12 @@ export interface DeliveryContent {
67
142
  readonly variables: Readonly<Record<string, string>>;
68
143
  readonly subject: string;
69
144
  readonly outboundMessageId: string;
145
+ /**
146
+ * Optional host-minted opaque reply-routing token rendered into the reply
147
+ * address. Indexed by SHA-256 in `support-desk.thread-index.v1`; never a
148
+ * read capability and never logged.
149
+ */
150
+ readonly replyRoutingToken?: string;
70
151
  }
71
152
  interface TicketRecord {
72
153
  readonly kind: "ticket";
@@ -93,7 +174,7 @@ interface AuditMemberRecord {
93
174
  readonly id: string;
94
175
  readonly event: AuditEvent;
95
176
  }
96
- type SupportCommandType = "create_customer_ticket" | "reply_customer_ticket" | "staff_reply" | "add_note" | "assign_ticket" | "change_priority" | "resolve_ticket" | "close_ticket" | "reopen_ticket";
177
+ type SupportCommandType = "create_customer_ticket" | "reply_customer_ticket" | "staff_reply" | "add_note" | "assign_ticket" | "change_priority" | "set_tags" | "resolve_ticket" | "close_ticket" | "reopen_ticket" | "inbound_email_create" | "inbound_email_reply" | "assistant_reply" | "assistant_escalate" | "request_person" | "take_over" | "confirm_customer_success";
97
178
  interface CommandRecord {
98
179
  readonly kind: "command";
99
180
  readonly partition: TicketId;
@@ -107,12 +188,78 @@ interface CommandRecord {
107
188
  readonly messageId?: MessageId;
108
189
  readonly requestFingerprint: string;
109
190
  readonly completedAt: IsoTimestamp;
191
+ /** New staff commands retain their result; messages are selected by revision. */
192
+ readonly resultTicket?: Ticket;
193
+ }
194
+ interface InboundReplyRerouteFenceRecord {
195
+ readonly kind: "inbound_reply_reroute_fence";
196
+ readonly partition: TicketId;
197
+ readonly id: string;
198
+ readonly commandId: string;
199
+ readonly messageId: MessageId;
200
+ readonly requestFingerprint: string;
201
+ readonly recordedAt: IsoTimestamp;
202
+ }
203
+ interface InboundCreateRerouteFenceRecord {
204
+ readonly kind: "inbound_create_reroute_fence";
205
+ readonly partition: TicketId;
206
+ readonly id: string;
207
+ readonly commandId: string;
208
+ readonly messageId: MessageId;
209
+ readonly requestFingerprint: string;
210
+ readonly recordedAt: IsoTimestamp;
110
211
  }
111
212
  interface TicketQuotaRecord {
112
213
  readonly kind: "ticket_quota";
113
214
  readonly partition: TicketId;
114
215
  readonly id: "quota";
115
216
  readonly messageCount: number;
217
+ /** Only explicitly opted-in web conversations reserve human completion capacity. */
218
+ readonly assistantManaged?: true;
219
+ }
220
+ export type AssistantEscalationReason = "no_guidance" | "unsuccessful" | "capacity" | "provider_failure" | "other";
221
+ export interface HandoffWorkRecord {
222
+ readonly kind: "handoff_work";
223
+ readonly partition: TicketId;
224
+ readonly id: string;
225
+ readonly status: "pending";
226
+ readonly reason: AssistantEscalationReason | "customer_requested" | "email_reply";
227
+ readonly summary?: string;
228
+ readonly attemptedGuideIds: readonly string[];
229
+ readonly transcript: {
230
+ readonly ticketId: TicketId;
231
+ readonly throughRevision: number;
232
+ readonly lastVisibleMessageId: MessageId;
233
+ };
234
+ readonly handlingGeneration: number;
235
+ readonly createdAt: IsoTimestamp;
236
+ }
237
+ /** Staff-only handoff context, separate from message/replay and assistant DTOs. */
238
+ export interface HandoffWorkDto {
239
+ readonly ticketId: TicketId;
240
+ readonly workId: string;
241
+ readonly status: HandoffWorkRecord["status"];
242
+ readonly reason: HandoffWorkRecord["reason"];
243
+ readonly summary?: string;
244
+ readonly attemptedGuideIds: readonly string[];
245
+ readonly transcript: HandoffWorkRecord["transcript"];
246
+ readonly handlingGeneration: number;
247
+ readonly createdAt: IsoTimestamp;
248
+ }
249
+ export interface CustomerSuccessConfirmation {
250
+ readonly commandId: string;
251
+ readonly ticketId: TicketId;
252
+ readonly confirmedBy: PrincipalId;
253
+ readonly confirmedAt: IsoTimestamp;
254
+ readonly lastSeenVisibleMessageId: MessageId;
255
+ readonly revision: number;
256
+ readonly handlingGeneration: number;
257
+ readonly resolutionCycle?: number;
258
+ }
259
+ interface CustomerSuccessRecord extends CustomerSuccessConfirmation {
260
+ readonly kind: "customer_success";
261
+ readonly partition: TicketId;
262
+ readonly id: string;
116
263
  }
117
264
  interface TicketReservationRecord {
118
265
  readonly kind: "ticket_reservation";
@@ -122,7 +269,13 @@ interface TicketReservationRecord {
122
269
  readonly generation: number;
123
270
  readonly state: "committed" | "cancelled";
124
271
  }
125
- export type SupportRecord = TicketRecord | TicketQuotaRecord | TicketReservationRecord | MessageRecord | AuditMemberRecord | CommandRecord | DeliveryJob;
272
+ export type SupportRecord = EngineeringFollowupRecord | EngineeringSourceRecord | EngineeringWorkRecord | EngineeringReceiptRecord | ResolutionLearningSourceRecord | ResolutionLearningWorkRecord | ResolutionLearningReviewRecord | {
273
+ readonly kind: "resolution_learning_receipt";
274
+ readonly partition: string;
275
+ readonly id: string;
276
+ readonly fingerprint: string;
277
+ readonly result: ResolutionLearningSourceResult;
278
+ } | AssistantTurnRecord | TicketRecord | TicketQuotaRecord | TicketReservationRecord | MessageRecord | AuditMemberRecord | CommandRecord | InboundReplyRerouteFenceRecord | InboundCreateRerouteFenceRecord | HandoffWorkRecord | CustomerSuccessRecord | DeliveryJob;
126
279
  /** Closed MVP registry of durable accepted-change audit action names. */
127
280
  export declare const supportTicketAuditActions: Readonly<{
128
281
  readonly created: "support.ticket.created";
@@ -132,9 +285,38 @@ export declare const supportTicketAuditActions: Readonly<{
132
285
  readonly assigned: "support.ticket.assigned";
133
286
  readonly unassigned: "support.ticket.unassigned";
134
287
  readonly priorityChanged: "support.ticket.priority_changed";
288
+ readonly tagsChanged: "support.ticket.tags_changed";
135
289
  readonly resolved: "support.ticket.resolved";
136
290
  readonly closed: "support.ticket.closed";
137
291
  readonly reopened: "support.ticket.reopened";
292
+ readonly assistantReplied: "support.ticket.assistant_replied";
293
+ readonly humanRequested: "support.ticket.human_requested";
294
+ readonly humanEscalated: "support.ticket.human_escalated";
295
+ readonly humanTakenOver: "support.ticket.human_taken_over";
296
+ readonly customerSuccessConfirmed: "support.ticket.customer_success_confirmed";
297
+ readonly learningEnrolled: "support.learning.enrolled";
298
+ readonly learningEvidenceRecorded: "support.learning.evidence_recorded";
299
+ readonly learningEvidenceInvalidated: "support.learning.evidence_invalidated";
300
+ readonly learningWithdrawn: "support.learning.withdrawn";
301
+ readonly learningReviewed: "support.learning.reviewed";
302
+ readonly engineeringEnrolled: "support.engineering.enrolled";
303
+ readonly engineeringEvidenceRecorded: "support.engineering.evidence_recorded";
304
+ readonly engineeringEvidenceInvalidated: "support.engineering.evidence_invalidated";
305
+ readonly engineeringWithdrawn: "support.engineering.withdrawn";
306
+ readonly engineeringRepositorySelected: "support.engineering.repository_selected";
307
+ readonly engineeringProposalPrepared: "support.engineering.proposal_prepared";
308
+ readonly engineeringReviewed: "support.engineering.reviewed";
309
+ readonly engineeringDispatchStarted: "support.engineering.dispatch_started";
310
+ readonly engineeringExportRecorded: "support.engineering.export_recorded";
311
+ readonly engineeringReconciled: "support.engineering.reconciled";
312
+ readonly engineeringFollowupObserved: "support.engineering.followup.observed";
313
+ readonly engineeringFollowupBundlePrepared: "support.engineering.followup.bundle_prepared";
314
+ readonly engineeringFollowupReassessed: "support.engineering.followup.reassessed";
315
+ readonly engineeringFollowupRetried: "support.engineering.followup.retried";
316
+ readonly engineeringFollowupAbandoned: "support.engineering.followup.abandoned";
317
+ readonly engineeringFollowupRejected: "support.engineering.followup.rejected";
318
+ readonly engineeringFollowupNoticePosted: "support.engineering.followup.notice_posted";
319
+ readonly engineeringFollowupRetestRecorded: "support.engineering.followup.retest_recorded";
138
320
  }>;
139
321
  export interface CustomerTicketIndexEntry {
140
322
  readonly ticketId: TicketId;
@@ -155,15 +337,61 @@ export interface InboundReceipt {
155
337
  readonly providerEventId: string;
156
338
  readonly externalMessageId?: string;
157
339
  readonly payloadFingerprint: string;
158
- readonly status: "processing" | "processed" | "rejected";
340
+ /** Canonical replay hash; absent on receipts written by earlier versions. */
341
+ readonly replayFingerprint?: string;
342
+ /** `rerouting` is a resumable claim used only to normalize an older route. */
343
+ readonly status: "processing" | "rerouting" | "processed" | "rejected";
159
344
  /** Trusted host time at which processing first reserved this receipt. */
160
345
  readonly receivedAt: IsoTimestamp;
161
346
  /** Trusted host time at which processing reached a terminal status. */
162
347
  readonly processedAt?: IsoTimestamp;
348
+ /** Resulting ticket; present only on a terminal receipt that names one. */
163
349
  readonly ticketId?: TicketId;
350
+ /** Resulting message; present only on a terminal receipt that names one. */
164
351
  readonly messageId?: MessageId;
165
352
  readonly diagnostic?: string;
353
+ /**
354
+ * Host-minted ticket ID reserved by the first delivery. Every retry of the
355
+ * same provider event reuses it instead of the retry's own identifiers.
356
+ */
357
+ readonly reservedTicketId?: TicketId;
358
+ /** Host-minted message ID reserved by the first delivery. */
359
+ readonly reservedMessageId?: MessageId;
360
+ readonly correlationId?: string;
361
+ /**
362
+ * First routing decision. Ordinary replays retain it; processing receipts
363
+ * written by older versions may be normalized before ticket work resumes.
364
+ */
365
+ readonly route?: InboundReceiptRoute;
366
+ readonly outcome?: InboundEmailOutcome;
166
367
  }
368
+ /** How inbound mail was associated with an existing ticket, if at all. */
369
+ export type InboundMatchMethod = "routing_token" | "message_id" | "subject_marker" | "none";
370
+ /** Coarse, content-free reason an inbound message created no message. */
371
+ export type InboundSuppressionReason = "auto_submitted" | "precedence" | "auto_reply_header" | "bounce" | "delivery_report" | "support_address_loop";
372
+ export type InboundEmailOutcome = "created" | "replied" | "duplicate_message" | "suppressed" | "rejected";
373
+ /** Routing decision persisted on the receipt before any ticket work. */
374
+ export type InboundReceiptRoute = {
375
+ readonly kind: "suppressed";
376
+ readonly reason: InboundSuppressionReason;
377
+ } | {
378
+ readonly kind: "duplicate_message";
379
+ readonly ticketId: TicketId;
380
+ readonly messageId: MessageId;
381
+ } | {
382
+ readonly kind: "reply";
383
+ readonly ticketId: TicketId;
384
+ readonly matchMethod: Exclude<InboundMatchMethod, "none">;
385
+ } | {
386
+ readonly kind: "new_conversation";
387
+ readonly ticketId: TicketId;
388
+ readonly matchMethod: InboundMatchMethod;
389
+ readonly association: "matched_email" | "unverified";
390
+ readonly principalId?: PrincipalId;
391
+ readonly priority: InboundRoutingPriority;
392
+ readonly category?: string;
393
+ readonly relatedTicketId?: TicketId;
394
+ };
167
395
  export interface DeliveryCallbackInput {
168
396
  readonly provider: string;
169
397
  readonly providerEventId: string;
@@ -221,10 +449,16 @@ export interface QueueIndexRecord {
221
449
  readonly status: TicketStatus;
222
450
  readonly priority: TicketPriority;
223
451
  readonly category?: string;
452
+ readonly tags?: readonly string[];
453
+ readonly firstResponseDueAt?: IsoTimestamp;
454
+ readonly resolutionDueAt?: IsoTimestamp;
455
+ readonly firstStaffRespondedAt?: IsoTimestamp;
456
+ readonly resolvedAt?: IsoTimestamp;
224
457
  readonly requesterAssociation: RequesterAssociation;
225
458
  readonly channel: TicketChannel;
226
459
  readonly assignedTo?: PrincipalId;
227
460
  readonly updatedAt: IsoTimestamp;
461
+ readonly handling?: TicketHandling;
228
462
  }
229
463
  export declare const queueIndex: import("@pegma/storage-core").CollectionDefinition<QueueIndexRecord>;
230
464
  /** Default terminal-retention cutoff shared by projection, repair, and sweep. */
@@ -255,6 +489,13 @@ export interface NotificationInput {
255
489
  readonly subject: string;
256
490
  readonly outboundMessageId: string;
257
491
  readonly maxAttempts?: number;
492
+ /**
493
+ * Optional opaque reply-routing token the host renders into this
494
+ * notification's reply address: 22 to 128 URL-safe base64 characters from
495
+ * a cryptographically random source (at least 128 bits). Support Desk
496
+ * indexes it so an inbound reply can be matched by key lookup.
497
+ */
498
+ readonly replyRoutingToken?: string;
258
499
  }
259
500
  export interface CreateCustomerTicketCommand {
260
501
  readonly commandId: string;
@@ -267,6 +508,7 @@ export interface CreateCustomerTicketCommand {
267
508
  readonly category?: string;
268
509
  readonly requesterEmail?: string;
269
510
  readonly notification?: NotificationInput;
511
+ readonly initialHandling?: "assistant";
270
512
  }
271
513
  /**
272
514
  * Instance-scoped positive ticket-number counter.
@@ -298,6 +540,7 @@ export interface CustomerTicketSummary {
298
540
  readonly channel: Ticket["channel"];
299
541
  readonly createdAt: Ticket["createdAt"];
300
542
  readonly customerUpdatedAt: Ticket["customerUpdatedAt"];
543
+ readonly handling?: TicketHandlingState;
301
544
  }
302
545
  /**
303
546
  * Customer-safe conversation fields. Omits principal IDs and provider
@@ -326,7 +569,58 @@ export interface StaffTicketView {
326
569
  readonly ticket: Ticket;
327
570
  readonly messages: readonly TicketMessage[];
328
571
  }
329
- export interface StaffReplyCommand {
572
+ /** Service-only safe conversation; never includes staff notes or requester identity. */
573
+ export interface AssistantConversationView extends CustomerTicketView {
574
+ readonly revision: number;
575
+ readonly handling: TicketHandling;
576
+ readonly triggerMessageId?: MessageId;
577
+ }
578
+ export interface AssistantTurnFence {
579
+ readonly commandId: string;
580
+ readonly correlationId: string;
581
+ readonly ticketId: TicketId;
582
+ readonly expectedRevision: number;
583
+ readonly expectedHandlingGeneration: number;
584
+ readonly triggerMessageId: MessageId;
585
+ }
586
+ export interface AssistantReplyCommand extends AssistantTurnFence {
587
+ readonly messageId: MessageId;
588
+ readonly body: string;
589
+ }
590
+ export interface AssistantEscalationCommand extends AssistantTurnFence {
591
+ readonly reason?: AssistantEscalationReason;
592
+ readonly summary?: string;
593
+ readonly attemptedGuideIds?: readonly string[];
594
+ }
595
+ export interface HandlingCommandResult {
596
+ readonly ticketId: TicketId;
597
+ readonly revision: number;
598
+ readonly handlingGeneration: number;
599
+ readonly messageId?: MessageId;
600
+ }
601
+ export interface RequestPersonCommand {
602
+ readonly commandId: string;
603
+ readonly correlationId: string;
604
+ readonly ticketId: TicketId;
605
+ }
606
+ export interface TakeOverTicketCommand extends RequestPersonCommand {
607
+ readonly expectedRevision: number;
608
+ readonly expectedHandlingGeneration: number;
609
+ }
610
+ export interface ConfirmCustomerSuccessCommand extends RequestPersonCommand {
611
+ readonly lastSeenVisibleMessageId: MessageId;
612
+ }
613
+ export interface StaffRevisionHint {
614
+ /** Omit to retain automatic conflict retries; supplied values must match. */
615
+ readonly expectedRevision?: number;
616
+ }
617
+ export interface SetTagsCommand extends StaffRevisionHint {
618
+ readonly commandId: string;
619
+ readonly correlationId: string;
620
+ readonly ticketId: TicketId;
621
+ readonly tags: readonly string[];
622
+ }
623
+ export interface StaffReplyCommand extends StaffRevisionHint {
330
624
  readonly commandId: string;
331
625
  readonly correlationId: string;
332
626
  readonly ticketId: TicketId;
@@ -334,14 +628,14 @@ export interface StaffReplyCommand {
334
628
  readonly body: string;
335
629
  readonly notification?: NotificationInput;
336
630
  }
337
- export interface AddNoteCommand {
631
+ export interface AddNoteCommand extends StaffRevisionHint {
338
632
  readonly commandId: string;
339
633
  readonly correlationId: string;
340
634
  readonly ticketId: TicketId;
341
635
  readonly messageId: MessageId;
342
636
  readonly body: string;
343
637
  }
344
- export interface AssignTicketCommand {
638
+ export interface AssignTicketCommand extends StaffRevisionHint {
345
639
  readonly commandId: string;
346
640
  readonly correlationId: string;
347
641
  readonly ticketId: TicketId;
@@ -356,13 +650,13 @@ export interface AssignTicketCommand {
356
650
  readonly body?: string;
357
651
  readonly notification?: NotificationInput;
358
652
  }
359
- export interface ChangePriorityCommand {
653
+ export interface ChangePriorityCommand extends StaffRevisionHint {
360
654
  readonly commandId: string;
361
655
  readonly correlationId: string;
362
656
  readonly ticketId: TicketId;
363
657
  readonly priority: TicketPriority;
364
658
  }
365
- export interface LifecycleTicketCommand {
659
+ export interface LifecycleTicketCommand extends StaffRevisionHint {
366
660
  readonly commandId: string;
367
661
  readonly correlationId: string;
368
662
  readonly ticketId: TicketId;
@@ -381,6 +675,14 @@ export interface LifecycleTicketCommand {
381
675
  * confirmation. Storage never filters or sorts projection rows.
382
676
  */
383
677
  export interface StaffQueueQuery {
678
+ /** Human work is the default; assistant/all are explicit supervision views. */
679
+ readonly handling?: "human" | "assistant" | "all";
680
+ readonly tag?: string;
681
+ /** Target filters and due sorting default to resolution. */
682
+ readonly target?: "first_response" | "resolution";
683
+ readonly targetState?: "overdue" | "due_soon";
684
+ /** Required for due_soon; inclusive horizon, at most 30 days. */
685
+ readonly dueSoonMilliseconds?: number;
384
686
  readonly status?: TicketStatus | readonly TicketStatus[];
385
687
  readonly priority?: TicketPriority | readonly TicketPriority[];
386
688
  readonly association?: RequesterAssociation | readonly RequesterAssociation[];
@@ -389,15 +691,21 @@ export interface StaffQueueQuery {
389
691
  readonly assignedTo?: PrincipalId;
390
692
  readonly unassignedOnly?: boolean;
391
693
  /** Newest updates first is the default staff presentation. */
392
- readonly sort?: "updated_newest" | "updated_oldest";
694
+ readonly sort?: "updated_newest" | "updated_oldest" | "due_soonest" | "due_latest";
393
695
  }
394
696
  /** Confirmed active ticket summary for the staff queue surface. */
395
697
  export interface StaffQueueItem {
698
+ readonly handling: TicketHandlingState;
396
699
  readonly ticketId: TicketId;
397
700
  readonly revision: number;
398
701
  readonly status: TicketStatus;
399
702
  readonly priority: TicketPriority;
400
703
  readonly category?: string;
704
+ readonly tags?: readonly string[];
705
+ readonly firstResponseDueAt?: IsoTimestamp;
706
+ readonly resolutionDueAt?: IsoTimestamp;
707
+ readonly firstStaffRespondedAt?: IsoTimestamp;
708
+ readonly resolvedAt?: IsoTimestamp;
401
709
  readonly requesterAssociation: RequesterAssociation;
402
710
  readonly channel: TicketChannel;
403
711
  readonly assignedTo?: PrincipalId;
@@ -427,7 +735,15 @@ export interface QueueInactiveSweepResult {
427
735
  readonly physicalRows: number;
428
736
  readonly deleted: number;
429
737
  }
430
- export interface SupportDeskApplication {
738
+ export interface SupportDeskApplication extends AssistantTurnApplication, ResolutionLearningApplication, EngineeringApplication, EngineeringFollowupApplication, EngineeringPolicyApplication {
739
+ readHandoffWork(access: AccessContext, ticketId: TicketId): Promise<readonly HandoffWorkDto[]>;
740
+ readAssistantConversation(access: AccessContext, ticketId: TicketId): Promise<AssistantConversationView>;
741
+ replyAsAssistant(access: AccessContext, command: AssistantReplyCommand): Promise<HandlingCommandResult>;
742
+ escalateToHuman(access: AccessContext, command: AssistantEscalationCommand): Promise<HandlingCommandResult>;
743
+ requestPerson(access: AccessContext, command: RequestPersonCommand): Promise<CustomerTicketView>;
744
+ takeOverTicket(access: AccessContext, command: TakeOverTicketCommand): Promise<StaffTicketView>;
745
+ confirmCustomerSuccess(access: AccessContext, command: ConfirmCustomerSuccessCommand): Promise<CustomerTicketView>;
746
+ readCustomerSuccessConfirmations(access: AccessContext, ticketId: TicketId): Promise<readonly CustomerSuccessConfirmation[]>;
431
747
  createCustomerTicket(access: AccessContext, command: CreateCustomerTicketCommand): Promise<CustomerTicketView>;
432
748
  replyToCustomerTicket(access: AccessContext, command: ReplyToCustomerTicketCommand): Promise<CustomerTicketView>;
433
749
  listCustomerTickets(access: AccessContext): Promise<readonly CustomerTicketSummary[]>;
@@ -447,12 +763,15 @@ export interface SupportDeskApplication {
447
763
  addNote(access: AccessContext, command: AddNoteCommand): Promise<StaffTicketView>;
448
764
  assignTicket(access: AccessContext, command: AssignTicketCommand): Promise<StaffTicketView>;
449
765
  changePriority(access: AccessContext, command: ChangePriorityCommand): Promise<StaffTicketView>;
766
+ setTags(access: AccessContext, command: SetTagsCommand): Promise<StaffTicketView>;
450
767
  resolveTicket(access: AccessContext, command: LifecycleTicketCommand): Promise<StaffTicketView>;
451
768
  closeTicket(access: AccessContext, command: LifecycleTicketCommand): Promise<StaffTicketView>;
452
769
  reopenTicket(access: AccessContext, command: LifecycleTicketCommand): Promise<StaffTicketView>;
453
770
  readTicketAuditHistory(access: AccessContext, ticketId: TicketId): Promise<readonly AuditEvent[]>;
454
771
  }
455
772
  export declare function validateOutboundMessageId(value: string, field?: string): string;
773
+ export declare const assistantHumanReserveRows = 12;
774
+ export declare const assistantHumanReserveMessages = 1;
456
775
  export interface ProjectTicketToQueueOptions {
457
776
  readonly store: Store;
458
777
  readonly clock: Clock;
@@ -503,6 +822,22 @@ export declare function createSupportDeskApplication(options: {
503
822
  * At most 32 values matching `^[a-z][a-z0-9_]{0,31}$`.
504
823
  */
505
824
  readonly allowedCategories?: readonly string[];
825
+ /** At most 32 normalized, deduplicated staff labels; absent disables adding tags. */
826
+ readonly allowedTags?: readonly string[];
827
+ /** Default 8, hard maximum 32. Empty tags always clear the ticket. */
828
+ readonly maxTagsPerTicket?: number;
829
+ readonly responseTargetPolicy?: ResponseTargetPolicy;
830
+ readonly assistantHandlingEnabled?: boolean;
831
+ /** Optional durable scheduling; absent retains the SD-01 manual assistant seam. */
832
+ readonly assistantTurns?: AssistantTurnOptions;
833
+ readonly resolutionLearning?: ResolutionLearningOptions;
834
+ readonly engineering?: EngineeringOptions;
835
+ readonly engineeringFollowup?: EngineeringFollowupOptions;
836
+ readonly engineeringPolicy?: {
837
+ readonly maxRepositories?: number;
838
+ readonly maxRecords?: number;
839
+ readonly historyRetentionMilliseconds?: number;
840
+ };
506
841
  }): SupportDeskApplication;
507
842
  export declare function inboundReceiptLocation(channelId: string, providerEventId: string): Promise<{
508
843
  readonly bucket: string;
@@ -531,5 +866,265 @@ export declare function sweepDeliveryCallbackReceipts(store: Store, clock: Clock
531
866
  export declare function pruneCustomerTicketIndex(store: Store, principalId: PrincipalId, input: {
532
867
  readonly reservedBefore: IsoTimestamp;
533
868
  }): Promise<readonly TicketId[]>;
534
- export {};
869
+ /** Which looked-up identifier a thread-index row resolves. */
870
+ export type ThreadIndexKind = "message_id" | "reply_token" | "ticket_number";
871
+ /**
872
+ * One key-lookup hint from an external identifier to a ticket.
873
+ *
874
+ * Partitions are `mid:<sha256>` for a `Message-ID`, `rt:<sha256>` for an
875
+ * opaque reply-routing token, and `number:<n>` for an instance ticket number,
876
+ * so every partition holds exactly one row and nothing grows without bound.
877
+ * A row is never authorization evidence: every lookup reloads the causal
878
+ * authoritative ticket or message and re-derives the row before trusting it.
879
+ * The reply token itself is never stored here, only its hash.
880
+ */
881
+ export interface ThreadIndexRecord {
882
+ /** Physical partition (see above). */
883
+ readonly key: string;
884
+ readonly id: "thread";
885
+ readonly kind: ThreadIndexKind;
886
+ readonly ticketId: TicketId;
887
+ /** Causal message for `message_id` and `reply_token` rows. */
888
+ readonly messageId?: MessageId;
889
+ /** Who generated an indexed `Message-ID`. */
890
+ readonly source?: "outbound" | "inbound";
891
+ /** Authoritative creation time of the causal message or ticket. */
892
+ readonly issuedAt: IsoTimestamp;
893
+ }
894
+ export declare const threadIndex: import("@pegma/storage-core").CollectionDefinition<ThreadIndexRecord>;
895
+ /**
896
+ * Default lifetime of `Message-ID` and reply-token threading rows. Lookups,
897
+ * repair, and sweeping all treat older rows as absent; ticket-number rows
898
+ * live as long as their ticket.
899
+ */
900
+ export declare const defaultThreadIndexRetentionMilliseconds: number;
901
+ /** Actor recorded on Audit events accepted from verified inbound mail. */
902
+ export declare const inboundEmailSystemActorId = "support-desk.inbound-email";
903
+ export interface ThreadIndexWorkerOptions {
904
+ readonly store: Store;
905
+ readonly clock: Clock;
906
+ /**
907
+ * Shared lifetime for `Message-ID` and reply-token rows. Hosts pass the
908
+ * value configured on the inbound processor to every repair and sweep
909
+ * invocation (no silent per-worker default).
910
+ */
911
+ readonly threadIndexRetentionMilliseconds: number;
912
+ /** Opaque adapter-issued continuation; omit to begin a new cycle. */
913
+ readonly cursor?: string;
914
+ /** Physical records per page; at most {@link MAX_SCAN_PAGE_SIZE}. */
915
+ readonly limit?: number;
916
+ }
917
+ export interface ThreadIndexRepairPageResult {
918
+ /** Host-persisted only after this whole page succeeds. */
919
+ readonly nextCursor: string | null;
920
+ readonly physicalRows: number;
921
+ readonly indexed: number;
922
+ readonly collisions: number;
923
+ }
924
+ export interface ThreadIndexSweepPageResult {
925
+ readonly nextCursor: string | null;
926
+ readonly physicalRows: number;
927
+ readonly deleted: number;
928
+ }
929
+ /**
930
+ * Scan one page of authoritative `support-desk.records.v1` rows and insert
931
+ * every thread-index row they imply (ticket numbers, outbound `Message-ID`s,
932
+ * reply tokens, and inbound `Message-ID`s). Rows past retention are not
933
+ * recreated. Hosts persist `nextCursor` only after the whole page succeeds
934
+ * and repeat complete cycles.
935
+ */
936
+ export declare function repairThreadIndexPage(options: ThreadIndexWorkerOptions): Promise<ThreadIndexRepairPageResult>;
937
+ /**
938
+ * Scan one page of thread-index rows and conditionally delete every row whose
939
+ * authoritative target is gone, no longer derives it, or is past retention.
940
+ * Uses `deleteIfUnchanged` with the scanned version.
941
+ */
942
+ export declare function sweepThreadIndexPage(options: ThreadIndexWorkerOptions): Promise<ThreadIndexSweepPageResult>;
943
+ /**
944
+ * Loop and bounce signals the host adapter copies from verified headers or
945
+ * the SMTP envelope. Support Desk only inspects them; it never stores them.
946
+ */
947
+ export interface InboundLoopSignals {
948
+ /** RFC 3834 `Auto-Submitted`; any value other than `no` suppresses. */
949
+ readonly autoSubmitted?: string;
950
+ /** `Precedence`; `bulk`, `junk`, `list`, or `auto_reply` suppresses. */
951
+ readonly precedence?: string;
952
+ /**
953
+ * `X-Auto-Response-Suppress`; `All`, `OOF`, or `AutoReply` keeps the message
954
+ * but forbids an automatic acknowledgement to the sender.
955
+ */
956
+ readonly autoResponseSuppress?: string;
957
+ /** True when `X-Autoreply`, `X-Autorespond`, or a similar header exists. */
958
+ readonly autoReplyHeaderPresent?: boolean;
959
+ /** `List-Id`; keeps the message but forbids an acknowledgement. */
960
+ readonly listId?: string;
961
+ /** Top-level `Content-Type`; `multipart/report` and delivery-status suppress. */
962
+ readonly contentType?: string;
963
+ /** SMTP envelope sender (`Return-Path`); empty or `<>` is a bounce. */
964
+ readonly envelopeSender?: string;
965
+ }
966
+ /**
967
+ * Provider-neutral inbound message, produced by a host adapter only after it
968
+ * verified the provider webhook or delivery mechanism, bounded MIME parsing,
969
+ * and reduced the content to safe plain text or constrained Markdown.
970
+ *
971
+ * Sender and content fields are hostile sender-controlled data. Identifiers
972
+ * are host-minted, and `senderAuthentication` is a host-evaluated result from
973
+ * trusted post-verification evidence. Raw MIME and HTML never belong here.
974
+ * There is deliberately no principal, role, plan, priority, or session field:
975
+ * sender authentication permits an email reply join but never establishes web
976
+ * identity or access.
977
+ */
978
+ export interface InboundEmailInput {
979
+ /** Host mailbox or channel identifier (an opaque name, not an address). */
980
+ readonly channelId: string;
981
+ /** Provider's stable event or delivery identifier for deduplication. */
982
+ readonly providerEventId: string;
983
+ /** Server-minted ID used only if this event creates a new ticket. */
984
+ readonly ticketId: TicketId;
985
+ /** Server-minted ID for the message this event may create. */
986
+ readonly messageId: MessageId;
987
+ readonly correlationId: string;
988
+ /** Sender address chosen by the host adapter; routing input only. */
989
+ readonly from: string;
990
+ /** Host-evaluated sender result bound to the normalized `from` address. */
991
+ readonly senderAuthentication?: InboundSenderAuthentication;
992
+ /** Untrusted display name; stored for staff context only. */
993
+ readonly fromName?: string;
994
+ /** Decoded subject; controls collapse and it is cut to the subject limit. */
995
+ readonly subject: string;
996
+ /** Normalized safe text with quoted history already bounded by the host. */
997
+ readonly body: string;
998
+ readonly format?: MessageFormat;
999
+ /** The inbound message's own `Message-ID`. */
1000
+ readonly externalMessageId?: string;
1001
+ readonly inReplyTo?: string;
1002
+ /** `References`, oldest first; only the newest entries are consulted. */
1003
+ readonly references?: readonly string[];
1004
+ /** Candidate opaque token the host extracted from the reply address. */
1005
+ readonly replyRoutingToken?: string;
1006
+ readonly loopSignals?: InboundLoopSignals;
1007
+ }
1008
+ /** Host account-email lookup result. Only `one` yields `matched_email`. */
1009
+ export type InboundSenderMatch = {
1010
+ readonly matches: "none";
1011
+ } | {
1012
+ readonly matches: "one";
1013
+ readonly principalId: PrincipalId;
1014
+ } | {
1015
+ readonly matches: "many";
1016
+ };
1017
+ /**
1018
+ * Host port that compares a sender address with known accounts. The answer
1019
+ * routes a ticket; it never authenticates the sender, never grants web
1020
+ * access, and is never used to choose an `AccessContext`.
1021
+ */
1022
+ export interface InboundAccountMatcher {
1023
+ matchSenderEmail(input: {
1024
+ readonly channelId: string;
1025
+ readonly email: string;
1026
+ }): Promise<InboundSenderMatch>;
1027
+ }
1028
+ /** Inbound mail may never be automatically assigned `urgent`. */
1029
+ export type InboundRoutingPriority = Exclude<TicketPriority, "urgent">;
1030
+ /** Content-free routing facts. Subject and body are deliberately absent. */
1031
+ export interface InboundRoutingInput {
1032
+ readonly channelId: string;
1033
+ readonly association: "matched_email" | "unverified";
1034
+ /** Present only for `matched_email`; routing context, not identity. */
1035
+ readonly principalId?: PrincipalId;
1036
+ readonly relatedTicketId?: TicketId;
1037
+ }
1038
+ export interface InboundRoutingDecision {
1039
+ readonly priority: InboundRoutingPriority;
1040
+ /** Optional category from the processor's configured allowlist. */
1041
+ readonly category?: string;
1042
+ }
1043
+ /** Host policy for a new inbound conversation (paid/unpaid tables live here). */
1044
+ export interface InboundRoutingPolicy {
1045
+ routeNewConversation(input: InboundRoutingInput): InboundRoutingDecision | Promise<InboundRoutingDecision>;
1046
+ }
1047
+ export interface InboundNotificationContext {
1048
+ readonly outcome: "created" | "replied";
1049
+ readonly channelId: string;
1050
+ readonly ticketId: TicketId;
1051
+ readonly ticketNumber: number;
1052
+ readonly messageId: MessageId;
1053
+ readonly association: RequesterAssociation;
1054
+ readonly matchMethod: InboundMatchMethod;
1055
+ readonly assignedTo?: PrincipalId;
1056
+ /**
1057
+ * False when the sender asked for no automatic responses or the message
1058
+ * came through a list. Hosts must then not acknowledge the sender; a staff
1059
+ * notification is still allowed.
1060
+ */
1061
+ readonly senderAcknowledgementAllowed: boolean;
1062
+ }
1063
+ /**
1064
+ * Optional host port returning one notification to commit in the same ticket
1065
+ * transaction as the inbound message (for example a staff alert). Never
1066
+ * called for suppressed, duplicate, or rejected mail.
1067
+ */
1068
+ export interface InboundNotificationPolicy {
1069
+ notificationFor(context: InboundNotificationContext): NotificationInput | undefined | Promise<NotificationInput | undefined>;
1070
+ }
1071
+ export interface InboundEmailResult {
1072
+ /** True when this provider event already had a receipt. */
1073
+ readonly duplicateDelivery: boolean;
1074
+ readonly outcome: InboundEmailOutcome;
1075
+ readonly ticketId?: TicketId;
1076
+ readonly messageId?: MessageId;
1077
+ readonly matchMethod?: InboundMatchMethod;
1078
+ /** Requester association of a newly created ticket. */
1079
+ readonly association?: RequesterAssociation;
1080
+ readonly relatedTicketId?: TicketId;
1081
+ /** Coarse content-free reason for `suppressed` or `rejected`. */
1082
+ readonly reason?: string;
1083
+ }
1084
+ export interface InboundMailProcessorOptions {
1085
+ readonly store: Store;
1086
+ readonly clock: Clock;
1087
+ readonly logger?: Logger;
1088
+ readonly accountMatcher: InboundAccountMatcher;
1089
+ readonly routingPolicy: InboundRoutingPolicy;
1090
+ readonly notificationPolicy?: InboundNotificationPolicy;
1091
+ /** Same limits as the application; subject and body bounds apply here. */
1092
+ readonly limits?: Partial<SupportDeskLimits>;
1093
+ readonly maxConflictAttempts?: number;
1094
+ /** Must equal the application's queue terminal-retention cutoff. */
1095
+ readonly queueTerminalRetentionMilliseconds?: number;
1096
+ /** Lifetime of `Message-ID` and reply-token rows; defaults to 365 days. */
1097
+ readonly threadIndexRetentionMilliseconds?: number;
1098
+ /** Frozen category allowlist for host routing decisions. */
1099
+ readonly allowedCategories?: readonly string[];
1100
+ readonly responseTargetPolicy?: ResponseTargetPolicy;
1101
+ /**
1102
+ * Instance subject-marker prefix, for example `SD` for `[SD-1042]`. Without
1103
+ * one, subject-marker fallback is disabled.
1104
+ */
1105
+ readonly subjectMarkerPrefix?: string;
1106
+ /** This instance's own support addresses; mail from them is a loop. */
1107
+ readonly supportAddresses?: readonly string[];
1108
+ }
1109
+ export interface InboundMailProcessor {
1110
+ /**
1111
+ * Deduplicate, route, and record one verified inbound message. Safe to
1112
+ * retry with the same provider event: a retry resumes the first delivery's
1113
+ * reserved identifiers and routing decision and returns its result.
1114
+ */
1115
+ processInboundEmail(input: InboundEmailInput): Promise<InboundEmailResult>;
1116
+ }
1117
+ /**
1118
+ * Create the inbound mail processor a host's mail worker calls after it has
1119
+ * verified provider authenticity and normalized bounded MIME content.
1120
+ *
1121
+ * Processing order: reserve the provider-event receipt with `insertIfAbsent`
1122
+ * before any ticket work; suppress bounce and auto-reply loops; persist one
1123
+ * routing decision on the receipt (routing token, then `In-Reply-To` /
1124
+ * `References`, then subject marker, each confirmed against authoritative
1125
+ * records); then create or reply in one ticket-partition transaction with
1126
+ * its Audit event, command receipt, and optional notification; finally mark
1127
+ * the receipt terminal and refresh the queue and threading projections.
1128
+ */
1129
+ export declare function createInboundMailProcessor(options: InboundMailProcessorOptions): InboundMailProcessor;
535
1130
  //# sourceMappingURL=index.d.ts.map