@pegma/support-desk-application 0.2.7 → 0.2.9
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.
- package/README.md +46 -20
- package/dist/engineering-followup-internal.d.ts +9 -2
- package/dist/engineering-followup-internal.d.ts.map +1 -1
- package/dist/engineering-followup-records.d.ts +12 -0
- package/dist/engineering-followup-records.d.ts.map +1 -1
- package/dist/engineering-followup-records.js +55 -5
- package/dist/engineering-followup-records.js.map +1 -1
- package/dist/engineering-followup-source.d.ts +18 -7
- package/dist/engineering-followup-source.d.ts.map +1 -1
- package/dist/engineering-followup-source.js +191 -23
- package/dist/engineering-followup-source.js.map +1 -1
- package/dist/engineering-followup-types.d.ts +75 -9
- package/dist/engineering-followup-types.d.ts.map +1 -1
- package/dist/engineering-followup-work.d.ts +9 -12
- package/dist/engineering-followup-work.d.ts.map +1 -1
- package/dist/engineering-followup-work.js +783 -234
- package/dist/engineering-followup-work.js.map +1 -1
- package/dist/engineering-handoff.d.ts +6 -0
- package/dist/engineering-handoff.d.ts.map +1 -0
- package/dist/engineering-handoff.js +31 -0
- package/dist/engineering-handoff.js.map +1 -0
- package/dist/engineering-types.d.ts +28 -0
- package/dist/engineering-types.d.ts.map +1 -1
- package/dist/engineering-work.d.ts +1 -1
- package/dist/engineering-work.d.ts.map +1 -1
- package/dist/engineering-work.js +534 -32
- package/dist/engineering-work.js.map +1 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +65 -4
- package/dist/index.js.map +1 -1
- package/dist/resolution-learning-delivery-handoff.d.ts +5 -0
- package/dist/resolution-learning-delivery-handoff.d.ts.map +1 -0
- package/dist/resolution-learning-delivery-handoff.js +20 -0
- package/dist/resolution-learning-delivery-handoff.js.map +1 -0
- package/dist/resolution-learning-types.d.ts +42 -1
- package/dist/resolution-learning-types.d.ts.map +1 -1
- package/dist/resolution-learning-worker.d.ts +1 -1
- package/dist/resolution-learning-worker.d.ts.map +1 -1
- package/dist/resolution-learning-worker.js +594 -369
- package/dist/resolution-learning-worker.js.map +1 -1
- package/package.json +3 -3
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { assertResolutionLearningReviewAttempt, } from "./resolution-learning-staged-records.js";
|
|
2
2
|
import { commitResolutionLearningReview } from "./resolution-learning-review-commit.js";
|
|
3
|
+
import { registerResolutionLearningDelivery } from "./resolution-learning-delivery-handoff.js";
|
|
3
4
|
import { resolutionLearningReviewIdentity, requireResolutionLearningReviewTicket, observeResolutionLearningReview, readResolutionLearningReviewSlot, resolutionLearningReviewReceiptAction, } from "./resolution-learning-review-identity.js";
|
|
4
5
|
import { hasPermission } from "@pegma/authorization-core";
|
|
5
6
|
import { MAX_SCAN_PAGE_SIZE } from "@pegma/storage-core";
|
|
@@ -93,6 +94,10 @@ function permission(access, review = false) {
|
|
|
93
94
|
if (review && !hasPermission(access, supportPermissions.queueRead))
|
|
94
95
|
throw new SupportDeskAuthorizationError(supportPermissions.queueRead);
|
|
95
96
|
}
|
|
97
|
+
function deliveryAssert(assertion) {
|
|
98
|
+
if (assertion !== undefined && assertion() !== true)
|
|
99
|
+
throw new SupportDeskConflictError("Delivery closing assertion refused");
|
|
100
|
+
}
|
|
96
101
|
function checkRow(row) {
|
|
97
102
|
if (encodedBytes(row) > 60_000)
|
|
98
103
|
throw new SupportDeskLimitError("serialized_record", 60_000);
|
|
@@ -304,7 +309,594 @@ export function createResolutionLearningWorkerServices(deps, load) {
|
|
|
304
309
|
recoveredModelPreparation,
|
|
305
310
|
};
|
|
306
311
|
}
|
|
312
|
+
function deliveryObservation(loaded, stored, actorId) {
|
|
313
|
+
let open = true;
|
|
314
|
+
let confirmed = false;
|
|
315
|
+
const check = (reader) => {
|
|
316
|
+
permission(reader);
|
|
317
|
+
if (!open || reader.principalId !== actorId)
|
|
318
|
+
throw new SupportDeskConflictError("Delivery observation expired or actor changed");
|
|
319
|
+
for (const name of [
|
|
320
|
+
supportPermissions.learningRead,
|
|
321
|
+
supportPermissions.queueRead,
|
|
322
|
+
])
|
|
323
|
+
if (!hasPermission(reader, name))
|
|
324
|
+
throw new SupportDeskAuthorizationError(name);
|
|
325
|
+
};
|
|
326
|
+
const observation = Object.freeze({
|
|
327
|
+
read(reader) {
|
|
328
|
+
check(reader);
|
|
329
|
+
return snapshot({ source: loaded.source.value, work: stored.value });
|
|
330
|
+
},
|
|
331
|
+
async confirm(reader) {
|
|
332
|
+
check(reader);
|
|
333
|
+
const source = await records.getVersioned(resolutionLearningSourceKey(stored.value.ticketId));
|
|
334
|
+
check(reader);
|
|
335
|
+
if (source?.version !== loaded.source.version)
|
|
336
|
+
throw new SupportDeskConflictError("Delivery Source changed");
|
|
337
|
+
const [ticket, work] = await Promise.allSettled([
|
|
338
|
+
records.getVersioned({
|
|
339
|
+
partition: stored.value.ticketId,
|
|
340
|
+
id: "ticket",
|
|
341
|
+
}),
|
|
342
|
+
records.getVersioned(resolutionLearningWorkKey(stored.value.ticketId, stored.value.sourceGeneration)),
|
|
343
|
+
]);
|
|
344
|
+
check(reader);
|
|
345
|
+
if (ticket.status === "rejected")
|
|
346
|
+
throw ticket.reason;
|
|
347
|
+
if (work.status === "rejected")
|
|
348
|
+
throw work.reason;
|
|
349
|
+
if (ticket.value?.version !== loaded.ticket.version ||
|
|
350
|
+
work.value?.version !== stored.version)
|
|
351
|
+
throw new SupportDeskConflictError("Delivery Ticket or Work changed");
|
|
352
|
+
confirmed = true;
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
return {
|
|
356
|
+
observation,
|
|
357
|
+
close() {
|
|
358
|
+
open = false;
|
|
359
|
+
},
|
|
360
|
+
ensure() {
|
|
361
|
+
if (!confirmed)
|
|
362
|
+
throw new SupportDeskConflictError("Delivery observation was not confirmed");
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
async function prepareStep(access, input, deliveryGuard) {
|
|
367
|
+
permission(access);
|
|
368
|
+
const command = snapshot(fields(input, "prepare learning", [
|
|
369
|
+
"ticketId",
|
|
370
|
+
"sourceGeneration",
|
|
371
|
+
"workerId",
|
|
372
|
+
"claimId",
|
|
373
|
+
"claimGeneration",
|
|
374
|
+
"step",
|
|
375
|
+
...(input.step === "model"
|
|
376
|
+
? ["binding"]
|
|
377
|
+
: [
|
|
378
|
+
"candidateHash",
|
|
379
|
+
"verifiedCheckReceipt",
|
|
380
|
+
"policyVersion",
|
|
381
|
+
"targetDecisionHash",
|
|
382
|
+
"exactRequest",
|
|
383
|
+
]),
|
|
384
|
+
]));
|
|
385
|
+
validateFence(command);
|
|
386
|
+
if (deliveryGuard !== undefined &&
|
|
387
|
+
(typeof deliveryGuard !== "function" || command.step !== "kb_delivery"))
|
|
388
|
+
throw new TypeError("Staged delivery preparation requires its guard");
|
|
389
|
+
const checkpoint = deliveryGuard === undefined
|
|
390
|
+
? undefined
|
|
391
|
+
: await fingerprint({ operation: "staged_delivery_prepare", command });
|
|
392
|
+
if (!options.enabled && deliveryGuard === undefined)
|
|
393
|
+
throw new SupportDeskConflictError("Learning processing disabled");
|
|
394
|
+
if (command.step !== "model" && command.step !== "kb_delivery")
|
|
395
|
+
throw new TypeError("invalid learning step");
|
|
396
|
+
for (let attempt = 0; attempt < (deliveryGuard === undefined ? deps.maxConflictAttempts : 1); attempt += 1) {
|
|
397
|
+
const loaded = await load(command.ticketId);
|
|
398
|
+
const stored = await workAt(command);
|
|
399
|
+
const work = stored.value;
|
|
400
|
+
if (checkpoint !== undefined &&
|
|
401
|
+
work.lastFinish?.fingerprint === checkpoint &&
|
|
402
|
+
work.exactRequest !== undefined)
|
|
403
|
+
return {
|
|
404
|
+
step: "kb_delivery",
|
|
405
|
+
stepId: work.stepId,
|
|
406
|
+
requestHash: work.requestHash,
|
|
407
|
+
};
|
|
408
|
+
if (!options.enabled)
|
|
409
|
+
throw new SupportDeskConflictError("Learning processing disabled");
|
|
410
|
+
live(loaded, work, command);
|
|
411
|
+
const savePrepared = async (next, nextSource = loaded.source.value) => {
|
|
412
|
+
if (deliveryGuard === undefined)
|
|
413
|
+
return save(loaded, stored, next, nextSource);
|
|
414
|
+
const scope = deliveryObservation(loaded, stored, access.principalId);
|
|
415
|
+
let assertion;
|
|
416
|
+
try {
|
|
417
|
+
assertion = await deliveryGuard(scope.observation);
|
|
418
|
+
scope.ensure();
|
|
419
|
+
}
|
|
420
|
+
finally {
|
|
421
|
+
scope.close();
|
|
422
|
+
}
|
|
423
|
+
if (assertion !== undefined && typeof assertion !== "function")
|
|
424
|
+
throw new TypeError("Invalid staged delivery assertion");
|
|
425
|
+
live(loaded, work, command);
|
|
426
|
+
const released = {
|
|
427
|
+
...withoutLease(next),
|
|
428
|
+
status: "prepared",
|
|
429
|
+
lastFinish: { fingerprint: checkpoint },
|
|
430
|
+
};
|
|
431
|
+
deliveryAssert(assertion);
|
|
432
|
+
const committed = await save(loaded, stored, released, nextSource);
|
|
433
|
+
if (committed)
|
|
434
|
+
deliveryAssert(assertion);
|
|
435
|
+
return committed;
|
|
436
|
+
};
|
|
437
|
+
let next;
|
|
438
|
+
let prepared;
|
|
439
|
+
let nextSource = loaded.source.value;
|
|
440
|
+
if (command.step === "model") {
|
|
441
|
+
const binding = fields(command.binding, "model binding", [
|
|
442
|
+
"inputManifest",
|
|
443
|
+
"inputFingerprint",
|
|
444
|
+
"inputManifestHash",
|
|
445
|
+
"configVersion",
|
|
446
|
+
"promptVersion",
|
|
447
|
+
"modelVersion",
|
|
448
|
+
"checkPolicyVersion",
|
|
449
|
+
"budgetsHash",
|
|
450
|
+
"usageReservationId",
|
|
451
|
+
"deadline",
|
|
452
|
+
]);
|
|
453
|
+
for (const name of Object.keys(binding).filter((key) => key !== "deadline" && key !== "inputManifest"))
|
|
454
|
+
text(binding[name], `binding.${name}`);
|
|
455
|
+
if ((await resolutionLearningManifestHash(command.binding.inputManifest)) !== command.binding.inputManifestHash)
|
|
456
|
+
throw new SupportDeskConflictError("Input manifest fingerprint mismatch");
|
|
457
|
+
const manifest = command.binding.inputManifest;
|
|
458
|
+
if (canonical(manifest.workingSolutionMessageIds) !==
|
|
459
|
+
canonical(work.workingSolutionMessageIds) ||
|
|
460
|
+
canonical(manifest.product) !==
|
|
461
|
+
canonical({
|
|
462
|
+
productId: work.product.productId,
|
|
463
|
+
version: work.product.version,
|
|
464
|
+
buildId: work.product.buildId,
|
|
465
|
+
binding: work.product.binding,
|
|
466
|
+
contextVersion: work.product.contextVersion,
|
|
467
|
+
}) ||
|
|
468
|
+
manifest.messages.some((reference) => !loaded.rows.some((row) => row.kind === "message" &&
|
|
469
|
+
row.message.visibility === "customer" &&
|
|
470
|
+
row.message.id === reference.messageId &&
|
|
471
|
+
row.ordinal === reference.ordinal &&
|
|
472
|
+
row.ordinal <= work.acceptedTicketRevision)))
|
|
473
|
+
throw new SupportDeskConflictError("Input manifest differs from authoritative source");
|
|
474
|
+
const deadline = timestamp(binding.deadline, "binding.deadline");
|
|
475
|
+
if (deadline <= timestamp(clock.now(), "now") ||
|
|
476
|
+
deadline > timestamp(work.deadline, "work.deadline") ||
|
|
477
|
+
deadline > timestamp(work.leaseUntil, "leaseUntil"))
|
|
478
|
+
throw new SupportDeskConflictError("Model deadline exceeds live claim");
|
|
479
|
+
const existing = work.modelAttempts.find((entry) => canonical(entry.binding) === canonical(command.binding));
|
|
480
|
+
if (existing !== undefined) {
|
|
481
|
+
if (existing.preparedClaimGeneration !== command.claimGeneration ||
|
|
482
|
+
existing.stepId !== work.stepId ||
|
|
483
|
+
work.stage !== "model")
|
|
484
|
+
throw new SupportDeskConflictError("Model preparation belongs to another invocation");
|
|
485
|
+
live(loaded, work, command);
|
|
486
|
+
if (await savePrepared(work))
|
|
487
|
+
return {
|
|
488
|
+
step: "model",
|
|
489
|
+
stepId: existing.stepId,
|
|
490
|
+
requestHash: existing.requestHash,
|
|
491
|
+
maxCandidateBytes: 16_000,
|
|
492
|
+
};
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
if (work.modelAttempts.some((entry) => entry.preparedClaimGeneration === command.claimGeneration))
|
|
496
|
+
throw new SupportDeskConflictError("This invocation already prepared a model request");
|
|
497
|
+
if (work.exactRequest !== undefined ||
|
|
498
|
+
work.candidate !== undefined ||
|
|
499
|
+
work.modelAttempts.length >= options.maxModelAttempts)
|
|
500
|
+
throw new SupportDeskConflictError("Learning model attempt budget exhausted");
|
|
501
|
+
const requestHash = await fingerprint(command.binding);
|
|
502
|
+
const stepId = await fingerprint({
|
|
503
|
+
ticketId: work.ticketId,
|
|
504
|
+
sourceGeneration: work.sourceGeneration,
|
|
505
|
+
modelAttempt: work.modelAttempts.length + 1,
|
|
506
|
+
requestHash,
|
|
507
|
+
});
|
|
508
|
+
next = {
|
|
509
|
+
...work,
|
|
510
|
+
stage: "model",
|
|
511
|
+
status: "prepared",
|
|
512
|
+
stepId,
|
|
513
|
+
requestHash,
|
|
514
|
+
workGeneration: work.workGeneration + 1,
|
|
515
|
+
modelAttempts: [
|
|
516
|
+
...work.modelAttempts,
|
|
517
|
+
{
|
|
518
|
+
preparedClaimGeneration: command.claimGeneration,
|
|
519
|
+
stepId,
|
|
520
|
+
requestHash,
|
|
521
|
+
binding: command.binding,
|
|
522
|
+
accounting: "unknown",
|
|
523
|
+
},
|
|
524
|
+
],
|
|
525
|
+
};
|
|
526
|
+
prepared = {
|
|
527
|
+
step: "model",
|
|
528
|
+
stepId,
|
|
529
|
+
requestHash,
|
|
530
|
+
maxCandidateBytes: 16_000,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
validateResolutionRequest(command.exactRequest);
|
|
535
|
+
text(command.candidateHash, "candidateHash");
|
|
536
|
+
text(command.verifiedCheckReceipt, "verifiedCheckReceipt");
|
|
537
|
+
text(command.policyVersion, "policyVersion");
|
|
538
|
+
text(command.targetDecisionHash, "targetDecisionHash");
|
|
539
|
+
if (work.candidateHash !== command.candidateHash ||
|
|
540
|
+
(work.candidate === undefined && work.exactRequest === undefined))
|
|
541
|
+
throw new SupportDeskConflictError("Candidate changed");
|
|
542
|
+
const reference = command.exactRequest.sourceReferences.find((item) => item.id === loaded.source.value.sourceId);
|
|
543
|
+
if (reference === undefined ||
|
|
544
|
+
reference.system !== "pegma.support-desk" ||
|
|
545
|
+
reference.resourceId !== work.ticketId ||
|
|
546
|
+
reference.version !== work.sourceBindingHash)
|
|
547
|
+
throw new SupportDeskConflictError("Exact source reference missing");
|
|
548
|
+
const applicability = work.candidate?.applicability ?? {
|
|
549
|
+
productId: work.product.productId,
|
|
550
|
+
version: work.product.version,
|
|
551
|
+
buildId: work.product.buildId,
|
|
552
|
+
};
|
|
553
|
+
const binding = await resolutionLearningDeliveryBinding({
|
|
554
|
+
candidateHash: command.candidateHash,
|
|
555
|
+
sourceBindingHash: work.sourceBindingHash,
|
|
556
|
+
policyVersion: command.policyVersion,
|
|
557
|
+
targetDecisionHash: command.targetDecisionHash,
|
|
558
|
+
applicability,
|
|
559
|
+
exactRequest: command.exactRequest,
|
|
560
|
+
});
|
|
561
|
+
const verified = options.checks === undefined
|
|
562
|
+
? null
|
|
563
|
+
: await options.checks.verify(command.verifiedCheckReceipt, binding, undefined, {
|
|
564
|
+
comparisonScope: {
|
|
565
|
+
collectionId: command.exactRequest.collectionId,
|
|
566
|
+
},
|
|
567
|
+
});
|
|
568
|
+
live(loaded, work, command);
|
|
569
|
+
if (verified === null ||
|
|
570
|
+
canonical(verified.binding) !== canonical(binding) ||
|
|
571
|
+
[
|
|
572
|
+
verified.privacy,
|
|
573
|
+
verified.factual,
|
|
574
|
+
verified.applicability,
|
|
575
|
+
verified.duplication,
|
|
576
|
+
].some((check) => check !== "passed") ||
|
|
577
|
+
timestamp(verified.validUntil, "check.validUntil") <
|
|
578
|
+
timestamp(clock.now(), "now"))
|
|
579
|
+
throw new SupportDeskConflictError("Independent learning checks missing or stale");
|
|
580
|
+
if (work.exactRequest !== undefined) {
|
|
581
|
+
if (work.requestHash !== binding.requestHash ||
|
|
582
|
+
canonical(work.exactRequest) !== canonical(command.exactRequest))
|
|
583
|
+
throw new SupportDeskConflictError("Prepared KB request cannot change");
|
|
584
|
+
if (await savePrepared(work))
|
|
585
|
+
return {
|
|
586
|
+
step: "kb_delivery",
|
|
587
|
+
stepId: work.stepId,
|
|
588
|
+
requestHash: work.requestHash,
|
|
589
|
+
};
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
const destination = nextSource.destinations.find((target) => target.articleId === command.exactRequest.articleId);
|
|
593
|
+
if (destination?.acceptance === "unknown" &&
|
|
594
|
+
(destination.reconciliation !== "confirmed" ||
|
|
595
|
+
destination.lastReconciliation?.proof === undefined))
|
|
596
|
+
throw new SupportDeskConflictError("Prior destination write still requires reconciliation");
|
|
597
|
+
if (destination === undefined &&
|
|
598
|
+
nextSource.destinations.length >= options.maxTargets)
|
|
599
|
+
throw new SupportDeskLimitError("ticket_partition", deps.limits.maxRecordsPerTicket);
|
|
600
|
+
const stepId = await fingerprint({
|
|
601
|
+
ticketId: work.ticketId,
|
|
602
|
+
sourceGeneration: work.sourceGeneration,
|
|
603
|
+
requestHash: binding.requestHash,
|
|
604
|
+
});
|
|
605
|
+
const { candidate: _candidate, ...rest } = work;
|
|
606
|
+
next = {
|
|
607
|
+
...rest,
|
|
608
|
+
stage: "kb_delivery",
|
|
609
|
+
status: "prepared",
|
|
610
|
+
stepId,
|
|
611
|
+
requestHash: binding.requestHash,
|
|
612
|
+
exactRequest: command.exactRequest,
|
|
613
|
+
verifiedCheckReceipt: command.verifiedCheckReceipt,
|
|
614
|
+
checkPolicyVersion: command.policyVersion,
|
|
615
|
+
targetDecisionHash: command.targetDecisionHash,
|
|
616
|
+
workGeneration: work.workGeneration + 1,
|
|
617
|
+
};
|
|
618
|
+
const entry = {
|
|
619
|
+
articleId: command.exactRequest.articleId,
|
|
620
|
+
sourceGeneration: work.sourceGeneration,
|
|
621
|
+
stepId,
|
|
622
|
+
requestHash: binding.requestHash,
|
|
623
|
+
acceptance: "unknown",
|
|
624
|
+
reconciliation: "pending",
|
|
625
|
+
};
|
|
626
|
+
nextSource = {
|
|
627
|
+
...nextSource,
|
|
628
|
+
reconciliationPending: true,
|
|
629
|
+
destinations: [
|
|
630
|
+
...nextSource.destinations.filter((target) => target.articleId !== entry.articleId),
|
|
631
|
+
entry,
|
|
632
|
+
],
|
|
633
|
+
workGeneration: nextSource.workGeneration + 1,
|
|
634
|
+
};
|
|
635
|
+
prepared = {
|
|
636
|
+
step: "kb_delivery",
|
|
637
|
+
stepId,
|
|
638
|
+
requestHash: binding.requestHash,
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
reserveReachable(next);
|
|
642
|
+
checkRow(nextSource);
|
|
643
|
+
if (await savePrepared(next, nextSource))
|
|
644
|
+
return prepared;
|
|
645
|
+
}
|
|
646
|
+
throw new SupportDeskConflictError();
|
|
647
|
+
}
|
|
648
|
+
async function completeStep(access, input, guard, plan) {
|
|
649
|
+
permission(access);
|
|
650
|
+
if (typeof guard !== "function")
|
|
651
|
+
throw new TypeError("Learning completion requires a read-only current-input guard");
|
|
652
|
+
let command = snapshot(fields(input, "complete learning", [
|
|
653
|
+
"ticketId",
|
|
654
|
+
"sourceGeneration",
|
|
655
|
+
"workerId",
|
|
656
|
+
"claimId",
|
|
657
|
+
"claimGeneration",
|
|
658
|
+
"stepId",
|
|
659
|
+
"requestHash",
|
|
660
|
+
"step",
|
|
661
|
+
...(input.step === "model"
|
|
662
|
+
? ["candidate", "accounting"]
|
|
663
|
+
: plan === undefined
|
|
664
|
+
? ["historicalAck", "currentTargetBinding"]
|
|
665
|
+
: []),
|
|
666
|
+
], input.step === "model" ? ["processing"] : []));
|
|
667
|
+
validateFence(command);
|
|
668
|
+
text(command.stepId, "stepId");
|
|
669
|
+
text(command.requestHash, "requestHash");
|
|
670
|
+
const completion = await fingerprint(plan === undefined
|
|
671
|
+
? command
|
|
672
|
+
: { operation: "staged_delivery_complete", command });
|
|
673
|
+
for (let attempt = 0; attempt < (plan === undefined ? deps.maxConflictAttempts : 1); attempt += 1) {
|
|
674
|
+
const loaded = await load(command.ticketId);
|
|
675
|
+
const stored = await workAt(command);
|
|
676
|
+
const work = stored.value;
|
|
677
|
+
if (work.lastCompletion?.fingerprint === completion) {
|
|
678
|
+
if (plan === undefined)
|
|
679
|
+
await guard();
|
|
680
|
+
return outcome(work);
|
|
681
|
+
}
|
|
682
|
+
if (plan !== undefined && !options.enabled)
|
|
683
|
+
throw new SupportDeskConflictError("Learning processing disabled");
|
|
684
|
+
live(loaded, work, command);
|
|
685
|
+
if (work.stepId !== command.stepId ||
|
|
686
|
+
work.requestHash !== command.requestHash ||
|
|
687
|
+
work.stage !== command.step)
|
|
688
|
+
throw new SupportDeskConflictError("Prepared learning step changed");
|
|
689
|
+
let planned;
|
|
690
|
+
if (plan !== undefined) {
|
|
691
|
+
if (command.step !== "kb_delivery" ||
|
|
692
|
+
work.acknowledgement === undefined)
|
|
693
|
+
throw new SupportDeskConflictError("Staged delivery acknowledgement missing");
|
|
694
|
+
const scope = deliveryObservation(loaded, stored, access.principalId);
|
|
695
|
+
try {
|
|
696
|
+
planned = await plan(scope.observation);
|
|
697
|
+
scope.ensure();
|
|
698
|
+
}
|
|
699
|
+
finally {
|
|
700
|
+
scope.close();
|
|
701
|
+
}
|
|
702
|
+
if (typeof planned?.assertCurrent !== "function")
|
|
703
|
+
throw new TypeError("Staged delivery completion requires a closing assertion");
|
|
704
|
+
validateResolutionTarget(planned.target);
|
|
705
|
+
command = {
|
|
706
|
+
...command,
|
|
707
|
+
historicalAck: work.acknowledgement,
|
|
708
|
+
currentTargetBinding: planned.target,
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
let next;
|
|
712
|
+
let nextSource = loaded.source.value;
|
|
713
|
+
if (command.step === "model") {
|
|
714
|
+
const modelCommand = command;
|
|
715
|
+
const preparedAttempt = work.modelAttempts.find((entry) => entry.stepId === command.stepId &&
|
|
716
|
+
entry.requestHash === command.requestHash);
|
|
717
|
+
if (preparedAttempt === undefined ||
|
|
718
|
+
preparedAttempt.preparedClaimGeneration !== command.claimGeneration)
|
|
719
|
+
throw new SupportDeskConflictError("Model completion belongs to another invocation");
|
|
720
|
+
validateResolutionCandidate(command.candidate);
|
|
721
|
+
if (!["accounted", "unknown"].includes(modelCommand.accounting))
|
|
722
|
+
throw new TypeError("Invalid model accounting");
|
|
723
|
+
processing(modelCommand.processing);
|
|
724
|
+
if (canonical(command.candidate.applicability) !==
|
|
725
|
+
canonical({
|
|
726
|
+
productId: work.product.productId,
|
|
727
|
+
version: work.product.version,
|
|
728
|
+
buildId: work.product.buildId,
|
|
729
|
+
}))
|
|
730
|
+
throw new SupportDeskConflictError("Candidate applicability differs from trusted product");
|
|
731
|
+
const candidateHash = await fingerprint(command.candidate);
|
|
732
|
+
next = {
|
|
733
|
+
...withoutLease(work),
|
|
734
|
+
status: "candidate",
|
|
735
|
+
candidate: command.candidate,
|
|
736
|
+
candidateHash,
|
|
737
|
+
workGeneration: work.workGeneration + 1,
|
|
738
|
+
lastCompletion: { fingerprint: completion },
|
|
739
|
+
modelAttempts: work.modelAttempts.map((entry) => entry.stepId !== command.stepId
|
|
740
|
+
? entry
|
|
741
|
+
: {
|
|
742
|
+
...entry,
|
|
743
|
+
accounting: modelCommand.accounting,
|
|
744
|
+
...(modelCommand.processing === undefined
|
|
745
|
+
? {}
|
|
746
|
+
: { processing: modelCommand.processing }),
|
|
747
|
+
}),
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
else {
|
|
751
|
+
const deliveryCommand = command;
|
|
752
|
+
validateResolutionTarget(command.currentTargetBinding);
|
|
753
|
+
const ack = fields(deliveryCommand.historicalAck, "KB acknowledgement", ["id", "generation", "articleRevision", "contentHash", "state"]);
|
|
754
|
+
for (const key of ["id", "contentHash", "state"])
|
|
755
|
+
text(ack[key], `ack.${key}`);
|
|
756
|
+
integer(ack.generation, "ack.generation", 1);
|
|
757
|
+
integer(ack.articleRevision, "ack.articleRevision", 1);
|
|
758
|
+
if (work.exactRequest === undefined ||
|
|
759
|
+
deliveryCommand.historicalAck.id !== work.exactRequest.articleId ||
|
|
760
|
+
command.currentTargetBinding.articleId !== work.exactRequest.articleId)
|
|
761
|
+
throw new SupportDeskConflictError("KB acknowledgement target mismatch");
|
|
762
|
+
const agrees = deliveryCommand.historicalAck.articleRevision ===
|
|
763
|
+
command.currentTargetBinding.articleRevision &&
|
|
764
|
+
deliveryCommand.historicalAck.contentHash ===
|
|
765
|
+
command.currentTargetBinding.contentHash;
|
|
766
|
+
next = {
|
|
767
|
+
...withoutLease(work),
|
|
768
|
+
status: agrees ? "completed" : "held",
|
|
769
|
+
acknowledgement: deliveryCommand.historicalAck,
|
|
770
|
+
target: command.currentTargetBinding,
|
|
771
|
+
workGeneration: work.workGeneration + 1,
|
|
772
|
+
lastCompletion: { fingerprint: completion },
|
|
773
|
+
...(agrees ? {} : { heldReason: "kb_target_changed" }),
|
|
774
|
+
};
|
|
775
|
+
nextSource = {
|
|
776
|
+
...nextSource,
|
|
777
|
+
workGeneration: nextSource.workGeneration + 1,
|
|
778
|
+
destinations: nextSource.destinations.map((entry) => entry.articleId === deliveryCommand.historicalAck.id
|
|
779
|
+
? {
|
|
780
|
+
...entry,
|
|
781
|
+
acceptance: "accepted",
|
|
782
|
+
reconciliation: agrees
|
|
783
|
+
? "confirmed"
|
|
784
|
+
: "pending",
|
|
785
|
+
}
|
|
786
|
+
: entry),
|
|
787
|
+
};
|
|
788
|
+
nextSource = {
|
|
789
|
+
...nextSource,
|
|
790
|
+
reconciliationPending: nextSource.destinations.some((entry) => entry.reconciliation !== "confirmed"),
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
checkRow(next);
|
|
794
|
+
if (planned === undefined)
|
|
795
|
+
await guard();
|
|
796
|
+
else
|
|
797
|
+
deliveryAssert(planned.assertCurrent);
|
|
798
|
+
live(loaded, work, command);
|
|
799
|
+
if (command.step === "model" &&
|
|
800
|
+
timestamp(work.modelAttempts.find((entry) => entry.stepId === command.stepId)
|
|
801
|
+
.binding.deadline, "model.deadline") <= timestamp(clock.now(), "now"))
|
|
802
|
+
throw new SupportDeskConflictError("Prepared model deadline expired");
|
|
803
|
+
if (await save(loaded, stored, next, nextSource)) {
|
|
804
|
+
if (planned !== undefined)
|
|
805
|
+
deliveryAssert(planned.assertCurrent);
|
|
806
|
+
return outcome(next);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
throw new SupportDeskConflictError();
|
|
810
|
+
}
|
|
811
|
+
const stagedDelivery = registerResolutionLearningDelivery(Object.freeze({
|
|
812
|
+
async prepare(access, command, guard) {
|
|
813
|
+
const prepared = await prepareStep(access, command, guard);
|
|
814
|
+
if (prepared.step !== "kb_delivery")
|
|
815
|
+
throw new SupportDeskConflictError("Delivery step changed");
|
|
816
|
+
return prepared;
|
|
817
|
+
},
|
|
818
|
+
async write(access, input, deliver) {
|
|
819
|
+
permission(access);
|
|
820
|
+
if (typeof deliver !== "function")
|
|
821
|
+
throw new TypeError("Delivery write requires its planner");
|
|
822
|
+
const command = snapshot(fields(input, "delivery write", [
|
|
823
|
+
"ticketId",
|
|
824
|
+
"sourceGeneration",
|
|
825
|
+
"workerId",
|
|
826
|
+
"claimId",
|
|
827
|
+
"claimGeneration",
|
|
828
|
+
"stepId",
|
|
829
|
+
"requestHash",
|
|
830
|
+
]));
|
|
831
|
+
validateFence(command);
|
|
832
|
+
text(command.stepId, "stepId");
|
|
833
|
+
text(command.requestHash, "requestHash");
|
|
834
|
+
const identity = await fingerprint({
|
|
835
|
+
operation: "staged_delivery_write",
|
|
836
|
+
command,
|
|
837
|
+
});
|
|
838
|
+
const loaded = await load(command.ticketId);
|
|
839
|
+
const stored = await workAt(command);
|
|
840
|
+
const work = stored.value;
|
|
841
|
+
if (work.lastFinish?.fingerprint === identity &&
|
|
842
|
+
work.acknowledgement !== undefined)
|
|
843
|
+
return outcome(work);
|
|
844
|
+
if (!options.enabled)
|
|
845
|
+
throw new SupportDeskConflictError("Learning processing disabled");
|
|
846
|
+
live(loaded, work, command);
|
|
847
|
+
if (work.stage !== "kb_delivery" ||
|
|
848
|
+
work.exactRequest === undefined ||
|
|
849
|
+
work.stepId !== command.stepId ||
|
|
850
|
+
work.requestHash !== command.requestHash ||
|
|
851
|
+
work.acknowledgement !== undefined)
|
|
852
|
+
throw new SupportDeskConflictError("Prepared delivery changed");
|
|
853
|
+
const scope = deliveryObservation(loaded, stored, access.principalId);
|
|
854
|
+
let planned;
|
|
855
|
+
try {
|
|
856
|
+
planned = await deliver(scope.observation);
|
|
857
|
+
scope.ensure();
|
|
858
|
+
}
|
|
859
|
+
finally {
|
|
860
|
+
scope.close();
|
|
861
|
+
}
|
|
862
|
+
if (typeof planned?.assertCurrent !== "function")
|
|
863
|
+
throw new TypeError("Delivery requires its closing assertion");
|
|
864
|
+
const ack = fields(planned.historicalAck, "KB acknowledgement", [
|
|
865
|
+
"id",
|
|
866
|
+
"generation",
|
|
867
|
+
"articleRevision",
|
|
868
|
+
"contentHash",
|
|
869
|
+
"state",
|
|
870
|
+
]);
|
|
871
|
+
for (const name of ["id", "contentHash", "state"])
|
|
872
|
+
text(ack[name], `ack.${name}`);
|
|
873
|
+
integer(ack.generation, "ack.generation", 1);
|
|
874
|
+
integer(ack.articleRevision, "ack.articleRevision", 1);
|
|
875
|
+
if (planned.historicalAck.id !== work.exactRequest.articleId)
|
|
876
|
+
throw new SupportDeskConflictError("Delivery acknowledgement changed");
|
|
877
|
+
const next = {
|
|
878
|
+
...withoutLease(work),
|
|
879
|
+
status: "prepared",
|
|
880
|
+
acknowledgement: snapshot(planned.historicalAck),
|
|
881
|
+
workGeneration: work.workGeneration + 1,
|
|
882
|
+
lastFinish: { fingerprint: identity },
|
|
883
|
+
};
|
|
884
|
+
reserveReachable(next);
|
|
885
|
+
live(loaded, work, command);
|
|
886
|
+
deliveryAssert(planned.assertCurrent);
|
|
887
|
+
if (!(await save(loaded, stored, next)))
|
|
888
|
+
throw new SupportDeskConflictError();
|
|
889
|
+
deliveryAssert(planned.assertCurrent);
|
|
890
|
+
return outcome(next);
|
|
891
|
+
},
|
|
892
|
+
complete(access, command, plan) {
|
|
893
|
+
if (typeof plan !== "function")
|
|
894
|
+
throw new TypeError("Delivery completion requires a planner");
|
|
895
|
+
return completeStep(access, { ...command, step: "kb_delivery" }, async () => { }, plan);
|
|
896
|
+
},
|
|
897
|
+
}));
|
|
307
898
|
return {
|
|
899
|
+
stagedResolutionLearningDelivery: stagedDelivery,
|
|
308
900
|
async scanResolutionLearningCandidates(access, input) {
|
|
309
901
|
permission(access);
|
|
310
902
|
const bounds = fields(input, "learning scan", ["limit"], ["cursor"]);
|
|
@@ -433,375 +1025,8 @@ export function createResolutionLearningWorkerServices(deps, load) {
|
|
|
433
1025
|
}
|
|
434
1026
|
throw new SupportDeskConflictError();
|
|
435
1027
|
},
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
const command = snapshot(fields(input, "prepare learning", [
|
|
439
|
-
"ticketId",
|
|
440
|
-
"sourceGeneration",
|
|
441
|
-
"workerId",
|
|
442
|
-
"claimId",
|
|
443
|
-
"claimGeneration",
|
|
444
|
-
"step",
|
|
445
|
-
...(input.step === "model"
|
|
446
|
-
? ["binding"]
|
|
447
|
-
: [
|
|
448
|
-
"candidateHash",
|
|
449
|
-
"verifiedCheckReceipt",
|
|
450
|
-
"policyVersion",
|
|
451
|
-
"targetDecisionHash",
|
|
452
|
-
"exactRequest",
|
|
453
|
-
]),
|
|
454
|
-
]));
|
|
455
|
-
validateFence(command);
|
|
456
|
-
if (!options.enabled)
|
|
457
|
-
throw new SupportDeskConflictError("Learning processing disabled");
|
|
458
|
-
if (command.step !== "model" && command.step !== "kb_delivery")
|
|
459
|
-
throw new TypeError("invalid learning step");
|
|
460
|
-
for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
|
|
461
|
-
const loaded = await load(command.ticketId);
|
|
462
|
-
const stored = await workAt(command);
|
|
463
|
-
const work = stored.value;
|
|
464
|
-
live(loaded, work, command);
|
|
465
|
-
let next;
|
|
466
|
-
let prepared;
|
|
467
|
-
let nextSource = loaded.source.value;
|
|
468
|
-
if (command.step === "model") {
|
|
469
|
-
const binding = fields(command.binding, "model binding", [
|
|
470
|
-
"inputManifest",
|
|
471
|
-
"inputFingerprint",
|
|
472
|
-
"inputManifestHash",
|
|
473
|
-
"configVersion",
|
|
474
|
-
"promptVersion",
|
|
475
|
-
"modelVersion",
|
|
476
|
-
"checkPolicyVersion",
|
|
477
|
-
"budgetsHash",
|
|
478
|
-
"usageReservationId",
|
|
479
|
-
"deadline",
|
|
480
|
-
]);
|
|
481
|
-
for (const name of Object.keys(binding).filter((key) => key !== "deadline" && key !== "inputManifest"))
|
|
482
|
-
text(binding[name], `binding.${name}`);
|
|
483
|
-
if ((await resolutionLearningManifestHash(command.binding.inputManifest)) !== command.binding.inputManifestHash)
|
|
484
|
-
throw new SupportDeskConflictError("Input manifest fingerprint mismatch");
|
|
485
|
-
const manifest = command.binding.inputManifest;
|
|
486
|
-
if (canonical(manifest.workingSolutionMessageIds) !==
|
|
487
|
-
canonical(work.workingSolutionMessageIds) ||
|
|
488
|
-
canonical(manifest.product) !==
|
|
489
|
-
canonical({
|
|
490
|
-
productId: work.product.productId,
|
|
491
|
-
version: work.product.version,
|
|
492
|
-
buildId: work.product.buildId,
|
|
493
|
-
binding: work.product.binding,
|
|
494
|
-
contextVersion: work.product.contextVersion,
|
|
495
|
-
}) ||
|
|
496
|
-
manifest.messages.some((reference) => !loaded.rows.some((row) => row.kind === "message" &&
|
|
497
|
-
row.message.visibility === "customer" &&
|
|
498
|
-
row.message.id === reference.messageId &&
|
|
499
|
-
row.ordinal === reference.ordinal &&
|
|
500
|
-
row.ordinal <= work.acceptedTicketRevision)))
|
|
501
|
-
throw new SupportDeskConflictError("Input manifest differs from authoritative source");
|
|
502
|
-
const deadline = timestamp(binding.deadline, "binding.deadline");
|
|
503
|
-
if (deadline <= timestamp(clock.now(), "now") ||
|
|
504
|
-
deadline > timestamp(work.deadline, "work.deadline") ||
|
|
505
|
-
deadline > timestamp(work.leaseUntil, "leaseUntil"))
|
|
506
|
-
throw new SupportDeskConflictError("Model deadline exceeds live claim");
|
|
507
|
-
const existing = work.modelAttempts.find((entry) => canonical(entry.binding) === canonical(command.binding));
|
|
508
|
-
if (existing !== undefined) {
|
|
509
|
-
if (existing.preparedClaimGeneration !== command.claimGeneration ||
|
|
510
|
-
existing.stepId !== work.stepId ||
|
|
511
|
-
work.stage !== "model")
|
|
512
|
-
throw new SupportDeskConflictError("Model preparation belongs to another invocation");
|
|
513
|
-
live(loaded, work, command);
|
|
514
|
-
if (await save(loaded, stored, work))
|
|
515
|
-
return {
|
|
516
|
-
step: "model",
|
|
517
|
-
stepId: existing.stepId,
|
|
518
|
-
requestHash: existing.requestHash,
|
|
519
|
-
maxCandidateBytes: 16_000,
|
|
520
|
-
};
|
|
521
|
-
continue;
|
|
522
|
-
}
|
|
523
|
-
if (work.modelAttempts.some((entry) => entry.preparedClaimGeneration === command.claimGeneration))
|
|
524
|
-
throw new SupportDeskConflictError("This invocation already prepared a model request");
|
|
525
|
-
if (work.exactRequest !== undefined ||
|
|
526
|
-
work.candidate !== undefined ||
|
|
527
|
-
work.modelAttempts.length >= options.maxModelAttempts)
|
|
528
|
-
throw new SupportDeskConflictError("Learning model attempt budget exhausted");
|
|
529
|
-
const requestHash = await fingerprint(command.binding);
|
|
530
|
-
const stepId = await fingerprint({
|
|
531
|
-
ticketId: work.ticketId,
|
|
532
|
-
sourceGeneration: work.sourceGeneration,
|
|
533
|
-
modelAttempt: work.modelAttempts.length + 1,
|
|
534
|
-
requestHash,
|
|
535
|
-
});
|
|
536
|
-
next = {
|
|
537
|
-
...work,
|
|
538
|
-
stage: "model",
|
|
539
|
-
status: "prepared",
|
|
540
|
-
stepId,
|
|
541
|
-
requestHash,
|
|
542
|
-
workGeneration: work.workGeneration + 1,
|
|
543
|
-
modelAttempts: [
|
|
544
|
-
...work.modelAttempts,
|
|
545
|
-
{
|
|
546
|
-
preparedClaimGeneration: command.claimGeneration,
|
|
547
|
-
stepId,
|
|
548
|
-
requestHash,
|
|
549
|
-
binding: command.binding,
|
|
550
|
-
accounting: "unknown",
|
|
551
|
-
},
|
|
552
|
-
],
|
|
553
|
-
};
|
|
554
|
-
prepared = {
|
|
555
|
-
step: "model",
|
|
556
|
-
stepId,
|
|
557
|
-
requestHash,
|
|
558
|
-
maxCandidateBytes: 16_000,
|
|
559
|
-
};
|
|
560
|
-
}
|
|
561
|
-
else {
|
|
562
|
-
validateResolutionRequest(command.exactRequest);
|
|
563
|
-
text(command.candidateHash, "candidateHash");
|
|
564
|
-
text(command.verifiedCheckReceipt, "verifiedCheckReceipt");
|
|
565
|
-
text(command.policyVersion, "policyVersion");
|
|
566
|
-
text(command.targetDecisionHash, "targetDecisionHash");
|
|
567
|
-
if (work.candidateHash !== command.candidateHash ||
|
|
568
|
-
(work.candidate === undefined && work.exactRequest === undefined))
|
|
569
|
-
throw new SupportDeskConflictError("Candidate changed");
|
|
570
|
-
const reference = command.exactRequest.sourceReferences.find((item) => item.id === loaded.source.value.sourceId);
|
|
571
|
-
if (reference === undefined ||
|
|
572
|
-
reference.system !== "pegma.support-desk" ||
|
|
573
|
-
reference.resourceId !== work.ticketId ||
|
|
574
|
-
reference.version !== work.sourceBindingHash)
|
|
575
|
-
throw new SupportDeskConflictError("Exact source reference missing");
|
|
576
|
-
const applicability = work.candidate?.applicability ?? {
|
|
577
|
-
productId: work.product.productId,
|
|
578
|
-
version: work.product.version,
|
|
579
|
-
buildId: work.product.buildId,
|
|
580
|
-
};
|
|
581
|
-
const binding = await resolutionLearningDeliveryBinding({
|
|
582
|
-
candidateHash: command.candidateHash,
|
|
583
|
-
sourceBindingHash: work.sourceBindingHash,
|
|
584
|
-
policyVersion: command.policyVersion,
|
|
585
|
-
targetDecisionHash: command.targetDecisionHash,
|
|
586
|
-
applicability,
|
|
587
|
-
exactRequest: command.exactRequest,
|
|
588
|
-
});
|
|
589
|
-
const verified = options.checks === undefined
|
|
590
|
-
? null
|
|
591
|
-
: await options.checks.verify(command.verifiedCheckReceipt, binding);
|
|
592
|
-
live(loaded, work, command);
|
|
593
|
-
if (verified === null ||
|
|
594
|
-
canonical(verified.binding) !== canonical(binding) ||
|
|
595
|
-
[
|
|
596
|
-
verified.privacy,
|
|
597
|
-
verified.factual,
|
|
598
|
-
verified.applicability,
|
|
599
|
-
verified.duplication,
|
|
600
|
-
].some((check) => check !== "passed") ||
|
|
601
|
-
timestamp(verified.validUntil, "check.validUntil") <
|
|
602
|
-
timestamp(clock.now(), "now"))
|
|
603
|
-
throw new SupportDeskConflictError("Independent learning checks missing or stale");
|
|
604
|
-
if (work.exactRequest !== undefined) {
|
|
605
|
-
if (work.requestHash !== binding.requestHash ||
|
|
606
|
-
canonical(work.exactRequest) !== canonical(command.exactRequest))
|
|
607
|
-
throw new SupportDeskConflictError("Prepared KB request cannot change");
|
|
608
|
-
if (await save(loaded, stored, work))
|
|
609
|
-
return {
|
|
610
|
-
step: "kb_delivery",
|
|
611
|
-
stepId: work.stepId,
|
|
612
|
-
requestHash: work.requestHash,
|
|
613
|
-
};
|
|
614
|
-
continue;
|
|
615
|
-
}
|
|
616
|
-
const destination = nextSource.destinations.find((target) => target.articleId === command.exactRequest.articleId);
|
|
617
|
-
if (destination?.acceptance === "unknown" &&
|
|
618
|
-
(destination.reconciliation !== "confirmed" ||
|
|
619
|
-
destination.lastReconciliation?.proof === undefined))
|
|
620
|
-
throw new SupportDeskConflictError("Prior destination write still requires reconciliation");
|
|
621
|
-
if (destination === undefined &&
|
|
622
|
-
nextSource.destinations.length >= options.maxTargets)
|
|
623
|
-
throw new SupportDeskLimitError("ticket_partition", deps.limits.maxRecordsPerTicket);
|
|
624
|
-
const stepId = await fingerprint({
|
|
625
|
-
ticketId: work.ticketId,
|
|
626
|
-
sourceGeneration: work.sourceGeneration,
|
|
627
|
-
requestHash: binding.requestHash,
|
|
628
|
-
});
|
|
629
|
-
const { candidate: _candidate, ...rest } = work;
|
|
630
|
-
next = {
|
|
631
|
-
...rest,
|
|
632
|
-
stage: "kb_delivery",
|
|
633
|
-
status: "prepared",
|
|
634
|
-
stepId,
|
|
635
|
-
requestHash: binding.requestHash,
|
|
636
|
-
exactRequest: command.exactRequest,
|
|
637
|
-
verifiedCheckReceipt: command.verifiedCheckReceipt,
|
|
638
|
-
checkPolicyVersion: command.policyVersion,
|
|
639
|
-
targetDecisionHash: command.targetDecisionHash,
|
|
640
|
-
workGeneration: work.workGeneration + 1,
|
|
641
|
-
};
|
|
642
|
-
const entry = {
|
|
643
|
-
articleId: command.exactRequest.articleId,
|
|
644
|
-
sourceGeneration: work.sourceGeneration,
|
|
645
|
-
stepId,
|
|
646
|
-
requestHash: binding.requestHash,
|
|
647
|
-
acceptance: "unknown",
|
|
648
|
-
reconciliation: "pending",
|
|
649
|
-
};
|
|
650
|
-
nextSource = {
|
|
651
|
-
...nextSource,
|
|
652
|
-
reconciliationPending: true,
|
|
653
|
-
destinations: [
|
|
654
|
-
...nextSource.destinations.filter((target) => target.articleId !== entry.articleId),
|
|
655
|
-
entry,
|
|
656
|
-
],
|
|
657
|
-
workGeneration: nextSource.workGeneration + 1,
|
|
658
|
-
};
|
|
659
|
-
prepared = {
|
|
660
|
-
step: "kb_delivery",
|
|
661
|
-
stepId,
|
|
662
|
-
requestHash: binding.requestHash,
|
|
663
|
-
};
|
|
664
|
-
}
|
|
665
|
-
reserveReachable(next);
|
|
666
|
-
checkRow(nextSource);
|
|
667
|
-
if (await save(loaded, stored, next, nextSource))
|
|
668
|
-
return prepared;
|
|
669
|
-
}
|
|
670
|
-
throw new SupportDeskConflictError();
|
|
671
|
-
},
|
|
672
|
-
async completeResolutionLearningStep(access, input, guard) {
|
|
673
|
-
permission(access);
|
|
674
|
-
if (typeof guard !== "function")
|
|
675
|
-
throw new TypeError("Learning completion requires a read-only current-input guard");
|
|
676
|
-
const command = snapshot(fields(input, "complete learning", [
|
|
677
|
-
"ticketId",
|
|
678
|
-
"sourceGeneration",
|
|
679
|
-
"workerId",
|
|
680
|
-
"claimId",
|
|
681
|
-
"claimGeneration",
|
|
682
|
-
"stepId",
|
|
683
|
-
"requestHash",
|
|
684
|
-
"step",
|
|
685
|
-
...(input.step === "model"
|
|
686
|
-
? ["candidate", "accounting"]
|
|
687
|
-
: ["historicalAck", "currentTargetBinding"]),
|
|
688
|
-
], input.step === "model" ? ["processing"] : []));
|
|
689
|
-
validateFence(command);
|
|
690
|
-
text(command.stepId, "stepId");
|
|
691
|
-
text(command.requestHash, "requestHash");
|
|
692
|
-
const completion = await fingerprint(command);
|
|
693
|
-
for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
|
|
694
|
-
const loaded = await load(command.ticketId);
|
|
695
|
-
const stored = await workAt(command);
|
|
696
|
-
const work = stored.value;
|
|
697
|
-
if (work.lastCompletion?.fingerprint === completion) {
|
|
698
|
-
await guard();
|
|
699
|
-
return outcome(work);
|
|
700
|
-
}
|
|
701
|
-
live(loaded, work, command);
|
|
702
|
-
if (work.stepId !== command.stepId ||
|
|
703
|
-
work.requestHash !== command.requestHash ||
|
|
704
|
-
work.stage !== command.step)
|
|
705
|
-
throw new SupportDeskConflictError("Prepared learning step changed");
|
|
706
|
-
let next;
|
|
707
|
-
let nextSource = loaded.source.value;
|
|
708
|
-
if (command.step === "model") {
|
|
709
|
-
const preparedAttempt = work.modelAttempts.find((entry) => entry.stepId === command.stepId &&
|
|
710
|
-
entry.requestHash === command.requestHash);
|
|
711
|
-
if (preparedAttempt === undefined ||
|
|
712
|
-
preparedAttempt.preparedClaimGeneration !== command.claimGeneration)
|
|
713
|
-
throw new SupportDeskConflictError("Model completion belongs to another invocation");
|
|
714
|
-
validateResolutionCandidate(command.candidate);
|
|
715
|
-
if (!["accounted", "unknown"].includes(command.accounting))
|
|
716
|
-
throw new TypeError("Invalid model accounting");
|
|
717
|
-
processing(command.processing);
|
|
718
|
-
if (canonical(command.candidate.applicability) !==
|
|
719
|
-
canonical({
|
|
720
|
-
productId: work.product.productId,
|
|
721
|
-
version: work.product.version,
|
|
722
|
-
buildId: work.product.buildId,
|
|
723
|
-
}))
|
|
724
|
-
throw new SupportDeskConflictError("Candidate applicability differs from trusted product");
|
|
725
|
-
const candidateHash = await fingerprint(command.candidate);
|
|
726
|
-
next = {
|
|
727
|
-
...withoutLease(work),
|
|
728
|
-
status: "candidate",
|
|
729
|
-
candidate: command.candidate,
|
|
730
|
-
candidateHash,
|
|
731
|
-
workGeneration: work.workGeneration + 1,
|
|
732
|
-
lastCompletion: { fingerprint: completion },
|
|
733
|
-
modelAttempts: work.modelAttempts.map((entry) => entry.stepId !== command.stepId
|
|
734
|
-
? entry
|
|
735
|
-
: {
|
|
736
|
-
...entry,
|
|
737
|
-
accounting: command.accounting,
|
|
738
|
-
...(command.processing === undefined
|
|
739
|
-
? {}
|
|
740
|
-
: { processing: command.processing }),
|
|
741
|
-
}),
|
|
742
|
-
};
|
|
743
|
-
}
|
|
744
|
-
else {
|
|
745
|
-
validateResolutionTarget(command.currentTargetBinding);
|
|
746
|
-
const ack = fields(command.historicalAck, "KB acknowledgement", [
|
|
747
|
-
"id",
|
|
748
|
-
"generation",
|
|
749
|
-
"articleRevision",
|
|
750
|
-
"contentHash",
|
|
751
|
-
"state",
|
|
752
|
-
]);
|
|
753
|
-
for (const key of ["id", "contentHash", "state"])
|
|
754
|
-
text(ack[key], `ack.${key}`);
|
|
755
|
-
integer(ack.generation, "ack.generation", 1);
|
|
756
|
-
integer(ack.articleRevision, "ack.articleRevision", 1);
|
|
757
|
-
if (work.exactRequest === undefined ||
|
|
758
|
-
command.historicalAck.id !== work.exactRequest.articleId ||
|
|
759
|
-
command.currentTargetBinding.articleId !==
|
|
760
|
-
work.exactRequest.articleId)
|
|
761
|
-
throw new SupportDeskConflictError("KB acknowledgement target mismatch");
|
|
762
|
-
const agrees = command.historicalAck.articleRevision ===
|
|
763
|
-
command.currentTargetBinding.articleRevision &&
|
|
764
|
-
command.historicalAck.contentHash ===
|
|
765
|
-
command.currentTargetBinding.contentHash;
|
|
766
|
-
next = {
|
|
767
|
-
...withoutLease(work),
|
|
768
|
-
status: agrees ? "completed" : "held",
|
|
769
|
-
acknowledgement: command.historicalAck,
|
|
770
|
-
target: command.currentTargetBinding,
|
|
771
|
-
workGeneration: work.workGeneration + 1,
|
|
772
|
-
lastCompletion: { fingerprint: completion },
|
|
773
|
-
...(agrees ? {} : { heldReason: "kb_target_changed" }),
|
|
774
|
-
};
|
|
775
|
-
nextSource = {
|
|
776
|
-
...nextSource,
|
|
777
|
-
workGeneration: nextSource.workGeneration + 1,
|
|
778
|
-
destinations: nextSource.destinations.map((entry) => entry.articleId === command.historicalAck.id
|
|
779
|
-
? {
|
|
780
|
-
...entry,
|
|
781
|
-
acceptance: "accepted",
|
|
782
|
-
reconciliation: agrees
|
|
783
|
-
? "confirmed"
|
|
784
|
-
: "pending",
|
|
785
|
-
}
|
|
786
|
-
: entry),
|
|
787
|
-
};
|
|
788
|
-
nextSource = {
|
|
789
|
-
...nextSource,
|
|
790
|
-
reconciliationPending: nextSource.destinations.some((entry) => entry.reconciliation !== "confirmed"),
|
|
791
|
-
};
|
|
792
|
-
}
|
|
793
|
-
checkRow(next);
|
|
794
|
-
await guard();
|
|
795
|
-
live(loaded, work, command);
|
|
796
|
-
if (command.step === "model" &&
|
|
797
|
-
timestamp(work.modelAttempts.find((entry) => entry.stepId === command.stepId)
|
|
798
|
-
.binding.deadline, "model.deadline") <= timestamp(clock.now(), "now"))
|
|
799
|
-
throw new SupportDeskConflictError("Prepared model deadline expired");
|
|
800
|
-
if (await save(loaded, stored, next, nextSource))
|
|
801
|
-
return outcome(next);
|
|
802
|
-
}
|
|
803
|
-
throw new SupportDeskConflictError();
|
|
804
|
-
},
|
|
1028
|
+
prepareResolutionLearningStep: (access, input) => prepareStep(access, input),
|
|
1029
|
+
completeResolutionLearningStep: (access, input, guard) => completeStep(access, input, guard),
|
|
805
1030
|
async finishResolutionLearningWork(access, input) {
|
|
806
1031
|
permission(access);
|
|
807
1032
|
const command = snapshot(fields(input, "finish learning", [
|