@pegma/support-desk-application 0.1.1 → 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 +6 -6
@@ -0,0 +1,1027 @@
1
+ import { hasPermission } from "@pegma/authorization-core";
2
+ import { MAX_SCAN_PAGE_SIZE } from "@pegma/storage-core";
3
+ import { SupportDeskAuthorizationError, SupportDeskConflictError, SupportDeskLimitError, SupportDeskNotFoundError, supportPermissions, } from "./index.js";
4
+ import { canonical, dataArray, encodedBytes, fields, fingerprint, integer, text, timestamp, } from "./assistant-turn-records.js";
5
+ import { safeLearningSnapshot as snapshot, assertResolutionLearningRecord, maximumResolutionLearningSourceBytes, resolutionLearningReviewKey, resolutionLearningSourceKey, resolutionLearningWorkKey, validateResolutionCandidate, validateResolutionRequest, validateResolutionTarget, } from "./resolution-learning-records.js";
6
+ export async function resolutionLearningManifestHash(manifest) {
7
+ const value = fields(manifest, "input manifest", [
8
+ "messages",
9
+ "workingSolutionMessageIds",
10
+ "product",
11
+ ]);
12
+ const seen = new Set();
13
+ for (const item of dataArray(value.messages, "manifest.messages", 100)) {
14
+ const message = fields(item, "manifest message", ["messageId", "ordinal"]);
15
+ text(message.messageId, "messageId");
16
+ integer(message.ordinal, "ordinal", 1);
17
+ if (seen.has(message.messageId))
18
+ throw new TypeError("Duplicate manifest message");
19
+ seen.add(message.messageId);
20
+ }
21
+ for (const id of dataArray(value.workingSolutionMessageIds, "solution IDs", 16)) {
22
+ text(id, "solution ID");
23
+ if (!seen.has(id))
24
+ throw new TypeError("Working solution absent from manifest");
25
+ }
26
+ const product = fields(value.product, "manifest.product", [
27
+ "productId",
28
+ "version",
29
+ "buildId",
30
+ "binding",
31
+ "contextVersion",
32
+ ]);
33
+ for (const key of Object.keys(product))
34
+ text(product[key], `manifest.product.${key}`);
35
+ return fingerprint(manifest);
36
+ }
37
+ function outcome(work) {
38
+ return {
39
+ ticketId: work.ticketId,
40
+ sourceGeneration: work.sourceGeneration,
41
+ workGeneration: work.workGeneration,
42
+ status: work.status,
43
+ ...(work.stepId === undefined ? {} : { stepId: work.stepId }),
44
+ ...(work.requestHash === undefined
45
+ ? {}
46
+ : { requestHash: work.requestHash }),
47
+ ...(work.candidateHash === undefined
48
+ ? {}
49
+ : { candidateHash: work.candidateHash }),
50
+ ...(work.target === undefined ? {} : { target: snapshot(work.target) }),
51
+ };
52
+ }
53
+ function processing(value) {
54
+ if (value === undefined)
55
+ return;
56
+ const object = fields(value, "processing", [], [
57
+ "provider",
58
+ "model",
59
+ "requestId",
60
+ "failureCategory",
61
+ "inputTokens",
62
+ "outputTokens",
63
+ "costMicros",
64
+ "currency",
65
+ ]);
66
+ for (const [name, entry] of Object.entries(object))
67
+ if (["inputTokens", "outputTokens", "costMicros"].includes(name))
68
+ integer(entry, `processing.${name}`, 0);
69
+ else
70
+ text(entry, `processing.${name}`);
71
+ }
72
+ export async function resolutionLearningDeliveryBinding(input) {
73
+ validateResolutionRequest(input.exactRequest);
74
+ return {
75
+ candidateHash: input.candidateHash,
76
+ sourceBindingHash: input.sourceBindingHash,
77
+ policyVersion: input.policyVersion,
78
+ targetDecisionHash: input.targetDecisionHash,
79
+ applicabilityHash: await fingerprint(input.applicability),
80
+ requestHash: await fingerprint(input.exactRequest),
81
+ };
82
+ }
83
+ function permission(access, review = false) {
84
+ text(access.principalId, "principalId");
85
+ const name = review
86
+ ? supportPermissions.learningReview
87
+ : supportPermissions.learningProcess;
88
+ if (!hasPermission(access, name))
89
+ throw new SupportDeskAuthorizationError(name);
90
+ if (review && !hasPermission(access, supportPermissions.queueRead))
91
+ throw new SupportDeskAuthorizationError(supportPermissions.queueRead);
92
+ }
93
+ function checkRow(row) {
94
+ if (encodedBytes(row) > 60_000)
95
+ throw new SupportDeskLimitError("serialized_record", 60_000);
96
+ if (row.kind === "resolution_learning_source" &&
97
+ maximumResolutionLearningSourceBytes(row) > 60_000)
98
+ throw new SupportDeskLimitError("serialized_record", 60_000);
99
+ assertResolutionLearningRecord(row, { partition: row.partition, id: row.id });
100
+ }
101
+ function validateFence(command) {
102
+ text(command.ticketId, "ticketId");
103
+ integer(command.sourceGeneration, "sourceGeneration", 1);
104
+ text(command.workerId, "workerId");
105
+ text(command.claimId, "claimId");
106
+ integer(command.claimGeneration, "claimGeneration", 1);
107
+ }
108
+ function sameClaim(work, command) {
109
+ return (work.claim !== undefined &&
110
+ canonical(work.claim) ===
111
+ canonical({
112
+ ticketId: command.ticketId,
113
+ sourceGeneration: command.sourceGeneration,
114
+ workerId: command.workerId,
115
+ claimId: command.claimId,
116
+ claimGeneration: command.claimGeneration,
117
+ }));
118
+ }
119
+ function current(loaded, work) {
120
+ return (loaded.source.value.state === "eligible" &&
121
+ loaded.source.value.sourceGeneration === work.sourceGeneration &&
122
+ loaded.source.value.resolutionCycle === work.resolutionCycle &&
123
+ loaded.source.value.sourceBindingHash === work.sourceBindingHash &&
124
+ loaded.ticket.value.ticket.resolvedAt === loaded.source.value.resolvedAt &&
125
+ ["resolved", "closed"].includes(loaded.ticket.value.ticket.status));
126
+ }
127
+ function withoutLease(work) {
128
+ const { claim: _claim, leaseUntil: _lease, retryAt: _retry, ...rest } = work;
129
+ return rest;
130
+ }
131
+ function reserveReachable(row) {
132
+ // Construct legal states for this prepared stage. Model completion retains
133
+ // its candidate; KB delivery has replaced that body with the exact request.
134
+ // They never share a candidate plus a KB acknowledgement/target envelope.
135
+ const escaped = "\ud800".repeat(200);
136
+ const processing = {
137
+ provider: escaped,
138
+ model: escaped,
139
+ requestId: escaped,
140
+ failureCategory: escaped,
141
+ currency: escaped,
142
+ inputTokens: Number.MAX_SAFE_INTEGER,
143
+ outputTokens: Number.MAX_SAFE_INTEGER,
144
+ costMicros: Number.MAX_SAFE_INTEGER,
145
+ };
146
+ const fence = {
147
+ ticketId: row.ticketId,
148
+ sourceGeneration: row.sourceGeneration,
149
+ workerId: escaped,
150
+ claimId: escaped,
151
+ claimGeneration: Number.MAX_SAFE_INTEGER,
152
+ };
153
+ const attempts = (accounting) => row.modelAttempts.map((attempt, index) => index === row.modelAttempts.length - 1
154
+ ? { ...attempt, accounting, processing }
155
+ : attempt);
156
+ const base = {
157
+ ...withoutLease(row),
158
+ workGeneration: Number.MAX_SAFE_INTEGER,
159
+ };
160
+ const timestampEnvelope = "+275760-09-13T00:00:00.000Z";
161
+ const finishStates = (future) => {
162
+ const finished = {
163
+ ...future,
164
+ status: "superseded",
165
+ heldReason: escaped,
166
+ retryAt: timestampEnvelope,
167
+ lastFinish: { fingerprint: "f".repeat(64) },
168
+ };
169
+ return [
170
+ finished,
171
+ {
172
+ ...finished,
173
+ status: "claimed",
174
+ claim: fence,
175
+ claimGeneration: Number.MAX_SAFE_INTEGER,
176
+ leaseUntil: timestampEnvelope,
177
+ },
178
+ ];
179
+ };
180
+ const reachable = [];
181
+ if (row.stage === "model") {
182
+ const completion = {
183
+ ...base,
184
+ status: "candidate",
185
+ modelAttempts: attempts("accounted"),
186
+ candidate: "\ud800".repeat(Math.ceil(16_000 / 12)),
187
+ candidateHash: "f".repeat(64),
188
+ lastCompletion: { fingerprint: "f".repeat(64) },
189
+ };
190
+ // Completion permits accounted/unknown; only a finish before a candidate
191
+ // can introduce the longer not_called disposition for this attempt.
192
+ reachable.push(completion, ...finishStates(completion));
193
+ reachable.push(...finishStates({ ...base, modelAttempts: attempts("not_called") }));
194
+ }
195
+ else {
196
+ const completion = {
197
+ ...base,
198
+ status: "completed",
199
+ lastCompletion: { fingerprint: "f".repeat(64) },
200
+ acknowledgement: {
201
+ id: escaped,
202
+ generation: Number.MAX_SAFE_INTEGER,
203
+ articleRevision: Number.MAX_SAFE_INTEGER,
204
+ contentHash: escaped,
205
+ state: escaped,
206
+ },
207
+ target: {
208
+ articleId: escaped,
209
+ collectionId: escaped,
210
+ articleRevision: Number.MAX_SAFE_INTEGER,
211
+ contentHash: escaped,
212
+ sourceSnapshotGeneration: Number.MAX_SAFE_INTEGER,
213
+ sourceSnapshotHash: escaped,
214
+ orderedReferencesHash: escaped,
215
+ },
216
+ };
217
+ reachable.push(completion, ...finishStates(completion));
218
+ }
219
+ for (const future of reachable)
220
+ if (encodedBytes(future) > 60_000)
221
+ throw new SupportDeskLimitError("serialized_record", 60_000);
222
+ checkRow(row);
223
+ }
224
+ export function createResolutionLearningWorkerServices(deps, load) {
225
+ const { records, clock, options } = deps;
226
+ async function workAt(key) {
227
+ const work = await records.getVersioned(resolutionLearningWorkKey(key.ticketId, key.sourceGeneration));
228
+ if (work?.value.kind !== "resolution_learning_work")
229
+ throw new SupportDeskNotFoundError();
230
+ return { value: work.value, version: work.version };
231
+ }
232
+ async function save(loaded, work, next, nextSource = loaded.source.value) {
233
+ checkRow(next);
234
+ checkRow(nextSource);
235
+ return (await records.transact(next.ticketId, [
236
+ {
237
+ action: "putIfUnchanged",
238
+ version: loaded.ticket.version,
239
+ value: loaded.ticket.value,
240
+ },
241
+ {
242
+ action: "putIfUnchanged",
243
+ version: loaded.source.version,
244
+ value: nextSource,
245
+ },
246
+ { action: "putIfUnchanged", version: work.version, value: next },
247
+ ])).committed;
248
+ }
249
+ function live(loaded, work, command) {
250
+ if (!sameClaim(work, command) ||
251
+ work.leaseUntil === undefined ||
252
+ timestamp(work.leaseUntil, "leaseUntil") <=
253
+ timestamp(clock.now(), "now") ||
254
+ !current(loaded, work))
255
+ throw new SupportDeskConflictError("Learning claim or source fence changed");
256
+ }
257
+ function claimView(loaded, work, recoveredModelPreparation) {
258
+ const seen = new Set();
259
+ const messages = loaded.rows
260
+ .flatMap((row) => {
261
+ if (row.kind !== "message" ||
262
+ row.message.visibility !== "customer" ||
263
+ row.ordinal > work.acceptedTicketRevision)
264
+ return [];
265
+ integer(row.ordinal, "ordinal", 1);
266
+ if (seen.has(row.ordinal))
267
+ throw new SupportDeskConflictError("Duplicate message ordinal");
268
+ seen.add(row.ordinal);
269
+ return [
270
+ {
271
+ messageId: row.message.id,
272
+ ordinal: row.ordinal,
273
+ speaker: row.message.authorKind === "customer"
274
+ ? "customer"
275
+ : row.message.authorKind === "assistant"
276
+ ? "assistant"
277
+ : row.message.authorKind === "system"
278
+ ? "notification"
279
+ : "support",
280
+ body: row.message.body,
281
+ },
282
+ ];
283
+ })
284
+ .sort((a, b) => a.ordinal - b.ordinal);
285
+ return {
286
+ fence: work.claim,
287
+ sourceFence: {
288
+ ticketId: work.ticketId,
289
+ sourceGeneration: work.sourceGeneration,
290
+ resolutionCycle: work.resolutionCycle,
291
+ sourceBindingHash: work.sourceBindingHash,
292
+ },
293
+ workGeneration: work.workGeneration,
294
+ leaseUntil: work.leaseUntil,
295
+ work: snapshot(work),
296
+ messages,
297
+ workingSolutionMessageIds: [...work.workingSolutionMessageIds],
298
+ recoveredModelPreparation,
299
+ };
300
+ }
301
+ return {
302
+ async scanResolutionLearningCandidates(access, input) {
303
+ permission(access);
304
+ const bounds = fields(input, "learning scan", ["limit"], ["cursor"]);
305
+ integer(bounds.limit, "limit", 1, MAX_SCAN_PAGE_SIZE);
306
+ if (bounds.cursor !== undefined)
307
+ text(bounds.cursor, "cursor", 20_000);
308
+ const page = await records.scan({
309
+ limit: bounds.limit,
310
+ ...(bounds.cursor === undefined
311
+ ? {}
312
+ : { cursor: bounds.cursor }),
313
+ });
314
+ const now = timestamp(clock.now(), "now");
315
+ const sources = new Map();
316
+ const candidates = [];
317
+ for (const row of page.records) {
318
+ if (row.value.kind !== "resolution_learning_work")
319
+ continue;
320
+ const work = row.value;
321
+ if (row.key.partition !== work.ticketId || row.key.id !== work.id)
322
+ throw new TypeError("Learning scan identity mismatch");
323
+ if (["completed", "superseded"].includes(work.status) ||
324
+ timestamp(work.deadline, "work.deadline") <= now)
325
+ continue;
326
+ let pending = sources.get(work.ticketId);
327
+ if (pending === undefined) {
328
+ pending = records
329
+ .get(resolutionLearningSourceKey(work.ticketId))
330
+ .then((value) => {
331
+ if (value === null)
332
+ return null;
333
+ if (value.kind !== "resolution_learning_source")
334
+ throw new SupportDeskConflictError("Learning source key contains another record");
335
+ return value;
336
+ });
337
+ sources.set(work.ticketId, pending);
338
+ }
339
+ const source = await pending;
340
+ if (source?.state === "eligible" &&
341
+ source.sourceGeneration === work.sourceGeneration &&
342
+ source.resolutionCycle === work.resolutionCycle &&
343
+ source.sourceBindingHash === work.sourceBindingHash)
344
+ candidates.push({
345
+ ticketId: work.ticketId,
346
+ sourceGeneration: work.sourceGeneration,
347
+ });
348
+ }
349
+ return {
350
+ candidates,
351
+ reconciliationSources: page.records.flatMap((row) => {
352
+ if (row.value.kind !== "resolution_learning_source" ||
353
+ !row.value.reconciliationPending)
354
+ return [];
355
+ if (row.key.partition !== row.value.ticketId ||
356
+ row.key.id !== "learning-source")
357
+ throw new TypeError("Learning reconciliation scan identity mismatch");
358
+ return [row.key.partition];
359
+ }),
360
+ nextCursor: page.nextCursor,
361
+ physicalRows: page.records.length,
362
+ pages: 1,
363
+ };
364
+ },
365
+ async claimResolutionLearningWork(access, input) {
366
+ permission(access);
367
+ const command = snapshot(fields(input, "claim learning", [
368
+ "ticketId",
369
+ "sourceGeneration",
370
+ "workerId",
371
+ "claimId",
372
+ "leaseMilliseconds",
373
+ ]));
374
+ text(command.ticketId, "ticketId");
375
+ integer(command.sourceGeneration, "sourceGeneration", 1);
376
+ text(command.workerId, "workerId");
377
+ text(command.claimId, "claimId");
378
+ integer(command.leaseMilliseconds, "leaseMilliseconds", 1, options.maxLeaseMilliseconds);
379
+ if (!options.enabled)
380
+ return null;
381
+ for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
382
+ const loaded = await load(command.ticketId);
383
+ const stored = await workAt(command);
384
+ const work = stored.value;
385
+ const now = timestamp(clock.now(), "now");
386
+ if (!current(loaded, work) ||
387
+ ["completed", "superseded"].includes(work.status) ||
388
+ now >= timestamp(work.deadline, "deadline") ||
389
+ (work.retryAt !== undefined &&
390
+ now < timestamp(work.retryAt, "retryAt")))
391
+ return null;
392
+ if (work.claim !== undefined &&
393
+ work.leaseUntil !== undefined &&
394
+ timestamp(work.leaseUntil, "leaseUntil") > now) {
395
+ if (work.claim.workerId !== command.workerId ||
396
+ work.claim.claimId !== command.claimId)
397
+ return null;
398
+ return claimView(loaded, work, work.stage === "model" && work.stepId !== undefined);
399
+ }
400
+ const claimGeneration = work.claim !== undefined &&
401
+ work.claim.workerId === command.workerId &&
402
+ work.claim.claimId === command.claimId &&
403
+ work.leaseUntil !== undefined &&
404
+ timestamp(work.leaseUntil, "leaseUntil") > now
405
+ ? work.claimGeneration
406
+ : work.claimGeneration + 1;
407
+ const fence = {
408
+ ticketId: command.ticketId,
409
+ sourceGeneration: command.sourceGeneration,
410
+ workerId: command.workerId,
411
+ claimId: command.claimId,
412
+ claimGeneration,
413
+ };
414
+ const next = {
415
+ ...work,
416
+ claimGeneration,
417
+ claim: fence,
418
+ leaseUntil: new Date(Math.min(now + command.leaseMilliseconds, timestamp(work.deadline, "deadline"))).toISOString(),
419
+ workGeneration: work.workGeneration + 1,
420
+ status: "claimed",
421
+ };
422
+ if (await save(loaded, stored, next)) {
423
+ return claimView(loaded, next, work.stage === "model" &&
424
+ work.stepId !== undefined &&
425
+ (work.status === "prepared" || work.status === "claimed"));
426
+ }
427
+ }
428
+ throw new SupportDeskConflictError();
429
+ },
430
+ async prepareResolutionLearningStep(access, input) {
431
+ permission(access);
432
+ const command = snapshot(fields(input, "prepare learning", [
433
+ "ticketId",
434
+ "sourceGeneration",
435
+ "workerId",
436
+ "claimId",
437
+ "claimGeneration",
438
+ "step",
439
+ ...(input.step === "model"
440
+ ? ["binding"]
441
+ : [
442
+ "candidateHash",
443
+ "verifiedCheckReceipt",
444
+ "policyVersion",
445
+ "targetDecisionHash",
446
+ "exactRequest",
447
+ ]),
448
+ ]));
449
+ validateFence(command);
450
+ if (!options.enabled)
451
+ throw new SupportDeskConflictError("Learning processing disabled");
452
+ if (command.step !== "model" && command.step !== "kb_delivery")
453
+ throw new TypeError("invalid learning step");
454
+ for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
455
+ const loaded = await load(command.ticketId);
456
+ const stored = await workAt(command);
457
+ const work = stored.value;
458
+ live(loaded, work, command);
459
+ let next;
460
+ let prepared;
461
+ let nextSource = loaded.source.value;
462
+ if (command.step === "model") {
463
+ const binding = fields(command.binding, "model binding", [
464
+ "inputManifest",
465
+ "inputFingerprint",
466
+ "inputManifestHash",
467
+ "configVersion",
468
+ "promptVersion",
469
+ "modelVersion",
470
+ "checkPolicyVersion",
471
+ "budgetsHash",
472
+ "usageReservationId",
473
+ "deadline",
474
+ ]);
475
+ for (const name of Object.keys(binding).filter((key) => key !== "deadline" && key !== "inputManifest"))
476
+ text(binding[name], `binding.${name}`);
477
+ if ((await resolutionLearningManifestHash(command.binding.inputManifest)) !== command.binding.inputManifestHash)
478
+ throw new SupportDeskConflictError("Input manifest fingerprint mismatch");
479
+ const manifest = command.binding.inputManifest;
480
+ if (canonical(manifest.workingSolutionMessageIds) !==
481
+ canonical(work.workingSolutionMessageIds) ||
482
+ canonical(manifest.product) !==
483
+ canonical({
484
+ productId: work.product.productId,
485
+ version: work.product.version,
486
+ buildId: work.product.buildId,
487
+ binding: work.product.binding,
488
+ contextVersion: work.product.contextVersion,
489
+ }) ||
490
+ manifest.messages.some((reference) => !loaded.rows.some((row) => row.kind === "message" &&
491
+ row.message.visibility === "customer" &&
492
+ row.message.id === reference.messageId &&
493
+ row.ordinal === reference.ordinal &&
494
+ row.ordinal <= work.acceptedTicketRevision)))
495
+ throw new SupportDeskConflictError("Input manifest differs from authoritative source");
496
+ const deadline = timestamp(binding.deadline, "binding.deadline");
497
+ if (deadline <= timestamp(clock.now(), "now") ||
498
+ deadline > timestamp(work.deadline, "work.deadline") ||
499
+ deadline > timestamp(work.leaseUntil, "leaseUntil"))
500
+ throw new SupportDeskConflictError("Model deadline exceeds live claim");
501
+ const existing = work.modelAttempts.find((entry) => canonical(entry.binding) === canonical(command.binding));
502
+ if (existing !== undefined) {
503
+ if (existing.preparedClaimGeneration !== command.claimGeneration ||
504
+ existing.stepId !== work.stepId ||
505
+ work.stage !== "model")
506
+ throw new SupportDeskConflictError("Model preparation belongs to another invocation");
507
+ live(loaded, work, command);
508
+ if (await save(loaded, stored, work))
509
+ return {
510
+ step: "model",
511
+ stepId: existing.stepId,
512
+ requestHash: existing.requestHash,
513
+ maxCandidateBytes: 16_000,
514
+ };
515
+ continue;
516
+ }
517
+ if (work.modelAttempts.some((entry) => entry.preparedClaimGeneration === command.claimGeneration))
518
+ throw new SupportDeskConflictError("This invocation already prepared a model request");
519
+ if (work.exactRequest !== undefined ||
520
+ work.candidate !== undefined ||
521
+ work.modelAttempts.length >= options.maxModelAttempts)
522
+ throw new SupportDeskConflictError("Learning model attempt budget exhausted");
523
+ const requestHash = await fingerprint(command.binding);
524
+ const stepId = await fingerprint({
525
+ ticketId: work.ticketId,
526
+ sourceGeneration: work.sourceGeneration,
527
+ modelAttempt: work.modelAttempts.length + 1,
528
+ requestHash,
529
+ });
530
+ next = {
531
+ ...work,
532
+ stage: "model",
533
+ status: "prepared",
534
+ stepId,
535
+ requestHash,
536
+ workGeneration: work.workGeneration + 1,
537
+ modelAttempts: [
538
+ ...work.modelAttempts,
539
+ {
540
+ preparedClaimGeneration: command.claimGeneration,
541
+ stepId,
542
+ requestHash,
543
+ binding: command.binding,
544
+ accounting: "unknown",
545
+ },
546
+ ],
547
+ };
548
+ prepared = {
549
+ step: "model",
550
+ stepId,
551
+ requestHash,
552
+ maxCandidateBytes: 16_000,
553
+ };
554
+ }
555
+ else {
556
+ validateResolutionRequest(command.exactRequest);
557
+ text(command.candidateHash, "candidateHash");
558
+ text(command.verifiedCheckReceipt, "verifiedCheckReceipt");
559
+ text(command.policyVersion, "policyVersion");
560
+ text(command.targetDecisionHash, "targetDecisionHash");
561
+ if (work.candidateHash !== command.candidateHash ||
562
+ (work.candidate === undefined && work.exactRequest === undefined))
563
+ throw new SupportDeskConflictError("Candidate changed");
564
+ const reference = command.exactRequest.sourceReferences.find((item) => item.id === loaded.source.value.sourceId);
565
+ if (reference === undefined ||
566
+ reference.system !== "pegma.support-desk" ||
567
+ reference.resourceId !== work.ticketId ||
568
+ reference.version !== work.sourceBindingHash)
569
+ throw new SupportDeskConflictError("Exact source reference missing");
570
+ const applicability = work.candidate?.applicability ?? {
571
+ productId: work.product.productId,
572
+ version: work.product.version,
573
+ buildId: work.product.buildId,
574
+ };
575
+ const binding = await resolutionLearningDeliveryBinding({
576
+ candidateHash: command.candidateHash,
577
+ sourceBindingHash: work.sourceBindingHash,
578
+ policyVersion: command.policyVersion,
579
+ targetDecisionHash: command.targetDecisionHash,
580
+ applicability,
581
+ exactRequest: command.exactRequest,
582
+ });
583
+ const verified = options.checks === undefined
584
+ ? null
585
+ : await options.checks.verify(command.verifiedCheckReceipt, binding);
586
+ live(loaded, work, command);
587
+ if (verified === null ||
588
+ canonical(verified.binding) !== canonical(binding) ||
589
+ [
590
+ verified.privacy,
591
+ verified.factual,
592
+ verified.applicability,
593
+ verified.duplication,
594
+ ].some((check) => check !== "passed") ||
595
+ timestamp(verified.validUntil, "check.validUntil") <
596
+ timestamp(clock.now(), "now"))
597
+ throw new SupportDeskConflictError("Independent learning checks missing or stale");
598
+ if (work.exactRequest !== undefined) {
599
+ if (work.requestHash !== binding.requestHash ||
600
+ canonical(work.exactRequest) !== canonical(command.exactRequest))
601
+ throw new SupportDeskConflictError("Prepared KB request cannot change");
602
+ if (await save(loaded, stored, work))
603
+ return {
604
+ step: "kb_delivery",
605
+ stepId: work.stepId,
606
+ requestHash: work.requestHash,
607
+ };
608
+ continue;
609
+ }
610
+ const destination = nextSource.destinations.find((target) => target.articleId === command.exactRequest.articleId);
611
+ if (destination?.acceptance === "unknown" &&
612
+ (destination.reconciliation !== "confirmed" ||
613
+ destination.lastReconciliation?.proof === undefined))
614
+ throw new SupportDeskConflictError("Prior destination write still requires reconciliation");
615
+ if (destination === undefined &&
616
+ nextSource.destinations.length >= options.maxTargets)
617
+ throw new SupportDeskLimitError("ticket_partition", deps.limits.maxRecordsPerTicket);
618
+ const stepId = await fingerprint({
619
+ ticketId: work.ticketId,
620
+ sourceGeneration: work.sourceGeneration,
621
+ requestHash: binding.requestHash,
622
+ });
623
+ const { candidate: _candidate, ...rest } = work;
624
+ next = {
625
+ ...rest,
626
+ stage: "kb_delivery",
627
+ status: "prepared",
628
+ stepId,
629
+ requestHash: binding.requestHash,
630
+ exactRequest: command.exactRequest,
631
+ verifiedCheckReceipt: command.verifiedCheckReceipt,
632
+ checkPolicyVersion: command.policyVersion,
633
+ targetDecisionHash: command.targetDecisionHash,
634
+ workGeneration: work.workGeneration + 1,
635
+ };
636
+ const entry = {
637
+ articleId: command.exactRequest.articleId,
638
+ sourceGeneration: work.sourceGeneration,
639
+ stepId,
640
+ requestHash: binding.requestHash,
641
+ acceptance: "unknown",
642
+ reconciliation: "pending",
643
+ };
644
+ nextSource = {
645
+ ...nextSource,
646
+ reconciliationPending: true,
647
+ destinations: [
648
+ ...nextSource.destinations.filter((target) => target.articleId !== entry.articleId),
649
+ entry,
650
+ ],
651
+ workGeneration: nextSource.workGeneration + 1,
652
+ };
653
+ prepared = {
654
+ step: "kb_delivery",
655
+ stepId,
656
+ requestHash: binding.requestHash,
657
+ };
658
+ }
659
+ reserveReachable(next);
660
+ checkRow(nextSource);
661
+ if (await save(loaded, stored, next, nextSource))
662
+ return prepared;
663
+ }
664
+ throw new SupportDeskConflictError();
665
+ },
666
+ async completeResolutionLearningStep(access, input, guard) {
667
+ permission(access);
668
+ if (typeof guard !== "function")
669
+ throw new TypeError("Learning completion requires a read-only current-input guard");
670
+ const command = snapshot(fields(input, "complete learning", [
671
+ "ticketId",
672
+ "sourceGeneration",
673
+ "workerId",
674
+ "claimId",
675
+ "claimGeneration",
676
+ "stepId",
677
+ "requestHash",
678
+ "step",
679
+ ...(input.step === "model"
680
+ ? ["candidate", "accounting"]
681
+ : ["historicalAck", "currentTargetBinding"]),
682
+ ], input.step === "model" ? ["processing"] : []));
683
+ validateFence(command);
684
+ text(command.stepId, "stepId");
685
+ text(command.requestHash, "requestHash");
686
+ const completion = await fingerprint(command);
687
+ for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
688
+ const loaded = await load(command.ticketId);
689
+ const stored = await workAt(command);
690
+ const work = stored.value;
691
+ if (work.lastCompletion?.fingerprint === completion) {
692
+ await guard();
693
+ return outcome(work);
694
+ }
695
+ live(loaded, work, command);
696
+ if (work.stepId !== command.stepId ||
697
+ work.requestHash !== command.requestHash ||
698
+ work.stage !== command.step)
699
+ throw new SupportDeskConflictError("Prepared learning step changed");
700
+ let next;
701
+ let nextSource = loaded.source.value;
702
+ if (command.step === "model") {
703
+ const preparedAttempt = work.modelAttempts.find((entry) => entry.stepId === command.stepId &&
704
+ entry.requestHash === command.requestHash);
705
+ if (preparedAttempt === undefined ||
706
+ preparedAttempt.preparedClaimGeneration !== command.claimGeneration)
707
+ throw new SupportDeskConflictError("Model completion belongs to another invocation");
708
+ validateResolutionCandidate(command.candidate);
709
+ if (!["accounted", "unknown"].includes(command.accounting))
710
+ throw new TypeError("Invalid model accounting");
711
+ processing(command.processing);
712
+ if (canonical(command.candidate.applicability) !==
713
+ canonical({
714
+ productId: work.product.productId,
715
+ version: work.product.version,
716
+ buildId: work.product.buildId,
717
+ }))
718
+ throw new SupportDeskConflictError("Candidate applicability differs from trusted product");
719
+ const candidateHash = await fingerprint(command.candidate);
720
+ next = {
721
+ ...withoutLease(work),
722
+ status: "candidate",
723
+ candidate: command.candidate,
724
+ candidateHash,
725
+ workGeneration: work.workGeneration + 1,
726
+ lastCompletion: { fingerprint: completion },
727
+ modelAttempts: work.modelAttempts.map((entry) => entry.stepId !== command.stepId
728
+ ? entry
729
+ : {
730
+ ...entry,
731
+ accounting: command.accounting,
732
+ ...(command.processing === undefined
733
+ ? {}
734
+ : { processing: command.processing }),
735
+ }),
736
+ };
737
+ }
738
+ else {
739
+ validateResolutionTarget(command.currentTargetBinding);
740
+ const ack = fields(command.historicalAck, "KB acknowledgement", [
741
+ "id",
742
+ "generation",
743
+ "articleRevision",
744
+ "contentHash",
745
+ "state",
746
+ ]);
747
+ for (const key of ["id", "contentHash", "state"])
748
+ text(ack[key], `ack.${key}`);
749
+ integer(ack.generation, "ack.generation", 1);
750
+ integer(ack.articleRevision, "ack.articleRevision", 1);
751
+ if (work.exactRequest === undefined ||
752
+ command.historicalAck.id !== work.exactRequest.articleId ||
753
+ command.currentTargetBinding.articleId !==
754
+ work.exactRequest.articleId)
755
+ throw new SupportDeskConflictError("KB acknowledgement target mismatch");
756
+ const agrees = command.historicalAck.articleRevision ===
757
+ command.currentTargetBinding.articleRevision &&
758
+ command.historicalAck.contentHash ===
759
+ command.currentTargetBinding.contentHash;
760
+ next = {
761
+ ...withoutLease(work),
762
+ status: agrees ? "completed" : "held",
763
+ acknowledgement: command.historicalAck,
764
+ target: command.currentTargetBinding,
765
+ workGeneration: work.workGeneration + 1,
766
+ lastCompletion: { fingerprint: completion },
767
+ ...(agrees ? {} : { heldReason: "kb_target_changed" }),
768
+ };
769
+ nextSource = {
770
+ ...nextSource,
771
+ workGeneration: nextSource.workGeneration + 1,
772
+ destinations: nextSource.destinations.map((entry) => entry.articleId === command.historicalAck.id
773
+ ? {
774
+ ...entry,
775
+ acceptance: "accepted",
776
+ reconciliation: agrees
777
+ ? "confirmed"
778
+ : "pending",
779
+ }
780
+ : entry),
781
+ };
782
+ nextSource = {
783
+ ...nextSource,
784
+ reconciliationPending: nextSource.destinations.some((entry) => entry.reconciliation !== "confirmed"),
785
+ };
786
+ }
787
+ checkRow(next);
788
+ await guard();
789
+ live(loaded, work, command);
790
+ if (command.step === "model" &&
791
+ timestamp(work.modelAttempts.find((entry) => entry.stepId === command.stepId)
792
+ .binding.deadline, "model.deadline") <= timestamp(clock.now(), "now"))
793
+ throw new SupportDeskConflictError("Prepared model deadline expired");
794
+ if (await save(loaded, stored, next, nextSource))
795
+ return outcome(next);
796
+ }
797
+ throw new SupportDeskConflictError();
798
+ },
799
+ async finishResolutionLearningWork(access, input) {
800
+ permission(access);
801
+ const command = snapshot(fields(input, "finish learning", [
802
+ "ticketId",
803
+ "sourceGeneration",
804
+ "workerId",
805
+ "claimId",
806
+ "claimGeneration",
807
+ "disposition",
808
+ ], [
809
+ "stepId",
810
+ "requestHash",
811
+ "accounting",
812
+ "retryAt",
813
+ "reason",
814
+ "processing",
815
+ ]));
816
+ validateFence(command);
817
+ if ((command.stepId === undefined) !==
818
+ (command.requestHash === undefined))
819
+ throw new TypeError("Learning finish requires both stepId and requestHash");
820
+ if (command.stepId !== undefined) {
821
+ text(command.stepId, "stepId");
822
+ text(command.requestHash, "requestHash");
823
+ }
824
+ if (!["retry", "held", "unknown", "superseded"].includes(command.disposition))
825
+ throw new TypeError("invalid learning disposition");
826
+ if (command.reason !== undefined)
827
+ text(command.reason, "reason");
828
+ processing(command.processing);
829
+ if (command.accounting !== undefined &&
830
+ !["not_called", "accounted", "unknown"].includes(command.accounting))
831
+ throw new TypeError("Invalid learning accounting");
832
+ const hash = await fingerprint(command);
833
+ for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
834
+ const loaded = await load(command.ticketId);
835
+ const stored = await workAt(command);
836
+ const work = stored.value;
837
+ if (work.lastFinish?.fingerprint === hash)
838
+ return outcome(work);
839
+ if (!sameClaim(work, command) ||
840
+ work.leaseUntil === undefined ||
841
+ timestamp(work.leaseUntil, "leaseUntil") <=
842
+ timestamp(clock.now(), "now"))
843
+ throw new SupportDeskConflictError("Learning claim expired");
844
+ if ((command.stepId !== undefined && command.stepId !== work.stepId) ||
845
+ (command.requestHash !== undefined &&
846
+ command.requestHash !== work.requestHash))
847
+ throw new SupportDeskConflictError("Learning finish step changed");
848
+ if (command.stepId === undefined &&
849
+ (work.exactRequest !== undefined ||
850
+ work.modelAttempts.some((entry) => entry.preparedClaimGeneration === command.claimGeneration)))
851
+ throw new SupportDeskConflictError("Prepared learning work requires its exact step fence");
852
+ const status = current(loaded, work) ? command.disposition : "held";
853
+ if (command.retryAt !== undefined &&
854
+ (timestamp(command.retryAt, "retryAt") >=
855
+ timestamp(work.deadline, "deadline") ||
856
+ timestamp(command.retryAt, "retryAt") <
857
+ timestamp(clock.now(), "now")))
858
+ throw new SupportDeskConflictError("Retry exceeds work deadline");
859
+ const next = {
860
+ ...withoutLease(work),
861
+ status,
862
+ workGeneration: work.workGeneration + 1,
863
+ lastFinish: { fingerprint: hash },
864
+ ...(command.retryAt === undefined
865
+ ? {}
866
+ : { retryAt: command.retryAt }),
867
+ ...(command.reason === undefined
868
+ ? {}
869
+ : { heldReason: command.reason }),
870
+ modelAttempts: work.modelAttempts.map((entry) => {
871
+ // A finish before preparation belongs to no provider request. A new
872
+ // claim cannot relabel an earlier invocation's known accounting.
873
+ if (command.stepId === undefined ||
874
+ command.requestHash === undefined ||
875
+ entry.stepId !== command.stepId ||
876
+ entry.requestHash !== command.requestHash)
877
+ return entry;
878
+ if (entry.preparedClaimGeneration !== command.claimGeneration) {
879
+ if (command.accounting !== undefined &&
880
+ command.accounting !== "unknown" &&
881
+ command.accounting !== entry.accounting)
882
+ throw new SupportDeskConflictError("Cannot rewrite another invocation's accounting");
883
+ return entry;
884
+ }
885
+ if (entry.accounting !== "unknown" &&
886
+ command.accounting !== undefined &&
887
+ command.accounting !== entry.accounting)
888
+ throw new SupportDeskConflictError("Known accounting cannot be replaced");
889
+ return {
890
+ ...entry,
891
+ ...(command.accounting === undefined
892
+ ? {}
893
+ : { accounting: command.accounting }),
894
+ ...(command.processing === undefined
895
+ ? {}
896
+ : { processing: command.processing }),
897
+ };
898
+ }),
899
+ };
900
+ if (await save(loaded, stored, next))
901
+ return outcome(next);
902
+ }
903
+ throw new SupportDeskConflictError();
904
+ },
905
+ async reviewResolutionLearningProposal(access, input, guard) {
906
+ permission(access, true);
907
+ if (typeof guard !== "function")
908
+ throw new TypeError("KB target review requires a read-only current-target guard");
909
+ const command = snapshot(fields(input, "review learning", [
910
+ "ticketId",
911
+ "sourceGeneration",
912
+ "commandId",
913
+ "correlationId",
914
+ "expectedWorkGeneration",
915
+ "target",
916
+ "decision",
917
+ "policyVersion",
918
+ ], ["reason"]));
919
+ validateResolutionTarget(command.target);
920
+ integer(command.expectedWorkGeneration, "expectedWorkGeneration", 1);
921
+ text(command.commandId, "commandId");
922
+ text(command.correlationId, "correlationId");
923
+ text(command.policyVersion, "policyVersion");
924
+ if (command.reason !== undefined)
925
+ text(command.reason, "reason");
926
+ if (command.decision !== "approve" && command.decision !== "reject")
927
+ throw new TypeError("invalid review decision");
928
+ const hash = await fingerprint({ command, actorId: access.principalId });
929
+ const key = {
930
+ partition: command.ticketId,
931
+ id: `learning-command:${command.commandId}`,
932
+ };
933
+ for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
934
+ const loaded = await load(command.ticketId);
935
+ const work = await workAt(command);
936
+ const prior = await records.get(key);
937
+ if (prior !== null) {
938
+ if (prior.kind !== "resolution_learning_receipt" ||
939
+ prior.fingerprint !== hash)
940
+ throw new SupportDeskConflictError("Review command reused");
941
+ return snapshot(prior.result);
942
+ }
943
+ if (!current(loaded, work.value) ||
944
+ work.value.workGeneration !== command.expectedWorkGeneration ||
945
+ !loaded.source.value.destinations.some((entry) => entry.articleId === command.target.articleId))
946
+ throw new SupportDeskConflictError("Review source/work/target changed");
947
+ const reviewKey = await resolutionLearningReviewKey(loaded.source.value, command.target, command.policyVersion);
948
+ const previousReview = await records.getVersioned(reviewKey);
949
+ if (previousReview !== null &&
950
+ previousReview.value.kind !== "resolution_learning_review")
951
+ throw new SupportDeskConflictError("Review binding key collision");
952
+ if (previousReview === null &&
953
+ loaded.source.value.reviewBindingCount >= options.maxReviewBindings)
954
+ throw new SupportDeskLimitError("ticket_partition", deps.limits.maxRecordsPerTicket);
955
+ deps.admission(loaded.ticket.value.ticket, loaded.rows, previousReview === null ? 3 : 2);
956
+ const now = clock.now();
957
+ const review = {
958
+ kind: "resolution_learning_review",
959
+ partition: command.ticketId,
960
+ id: reviewKey.id,
961
+ ticketId: command.ticketId,
962
+ sourceGeneration: loaded.source.value.sourceGeneration,
963
+ sourceBindingHash: loaded.source.value.sourceBindingHash,
964
+ target: command.target,
965
+ decision: command.decision,
966
+ policyVersion: command.policyVersion,
967
+ reviewerId: access.principalId,
968
+ reviewedAt: now,
969
+ ...(command.reason === undefined ? {} : { reason: command.reason }),
970
+ };
971
+ assertResolutionLearningRecord(review, reviewKey);
972
+ const source = {
973
+ ...loaded.source.value,
974
+ workGeneration: loaded.source.value.workGeneration + 1,
975
+ reviewBindingCount: loaded.source.value.reviewBindingCount +
976
+ (previousReview === null ? 1 : 0),
977
+ };
978
+ checkRow(source);
979
+ const outcome = {
980
+ ticketId: source.ticketId,
981
+ sourceGeneration: source.sourceGeneration,
982
+ resolutionCycle: source.resolutionCycle,
983
+ state: source.state,
984
+ };
985
+ await guard();
986
+ if ((await records.transact(command.ticketId, [
987
+ {
988
+ action: "putIfUnchanged",
989
+ version: loaded.ticket.version,
990
+ value: loaded.ticket.value,
991
+ },
992
+ {
993
+ action: "putIfUnchanged",
994
+ version: loaded.source.version,
995
+ value: source,
996
+ },
997
+ {
998
+ action: "putIfUnchanged",
999
+ version: work.version,
1000
+ value: work.value,
1001
+ },
1002
+ previousReview === null
1003
+ ? { action: "insert", value: review }
1004
+ : {
1005
+ action: "putIfUnchanged",
1006
+ version: previousReview.version,
1007
+ value: review,
1008
+ },
1009
+ deps.audit(command, loaded.ticket.value.ticket, access.principalId, now, "support.learning.reviewed"),
1010
+ {
1011
+ action: "insert",
1012
+ value: {
1013
+ kind: "resolution_learning_receipt",
1014
+ partition: command.ticketId,
1015
+ id: key.id,
1016
+ fingerprint: hash,
1017
+ result: outcome,
1018
+ },
1019
+ },
1020
+ ])).committed)
1021
+ return outcome;
1022
+ }
1023
+ throw new SupportDeskConflictError();
1024
+ },
1025
+ };
1026
+ }
1027
+ //# sourceMappingURL=resolution-learning-worker.js.map