@pegma/support-desk-application 0.2.8 → 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.
Files changed (40) hide show
  1. package/README.md +46 -22
  2. package/dist/engineering-followup-records.d.ts +12 -0
  3. package/dist/engineering-followup-records.d.ts.map +1 -1
  4. package/dist/engineering-followup-records.js +55 -5
  5. package/dist/engineering-followup-records.js.map +1 -1
  6. package/dist/engineering-followup-source.d.ts +8 -0
  7. package/dist/engineering-followup-source.d.ts.map +1 -1
  8. package/dist/engineering-followup-source.js +96 -18
  9. package/dist/engineering-followup-source.js.map +1 -1
  10. package/dist/engineering-followup-types.d.ts +45 -3
  11. package/dist/engineering-followup-types.d.ts.map +1 -1
  12. package/dist/engineering-followup-work.d.ts +5 -5
  13. package/dist/engineering-followup-work.d.ts.map +1 -1
  14. package/dist/engineering-followup-work.js +471 -107
  15. package/dist/engineering-followup-work.js.map +1 -1
  16. package/dist/engineering-handoff.d.ts +6 -0
  17. package/dist/engineering-handoff.d.ts.map +1 -0
  18. package/dist/engineering-handoff.js +31 -0
  19. package/dist/engineering-handoff.js.map +1 -0
  20. package/dist/engineering-types.d.ts +28 -0
  21. package/dist/engineering-types.d.ts.map +1 -1
  22. package/dist/engineering-work.d.ts +1 -1
  23. package/dist/engineering-work.d.ts.map +1 -1
  24. package/dist/engineering-work.js +534 -32
  25. package/dist/engineering-work.js.map +1 -1
  26. package/dist/index.d.ts +13 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +49 -2
  29. package/dist/index.js.map +1 -1
  30. package/dist/resolution-learning-delivery-handoff.d.ts +5 -0
  31. package/dist/resolution-learning-delivery-handoff.d.ts.map +1 -0
  32. package/dist/resolution-learning-delivery-handoff.js +20 -0
  33. package/dist/resolution-learning-delivery-handoff.js.map +1 -0
  34. package/dist/resolution-learning-types.d.ts +42 -1
  35. package/dist/resolution-learning-types.d.ts.map +1 -1
  36. package/dist/resolution-learning-worker.d.ts +1 -1
  37. package/dist/resolution-learning-worker.d.ts.map +1 -1
  38. package/dist/resolution-learning-worker.js +594 -369
  39. package/dist/resolution-learning-worker.js.map +1 -1
  40. 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
- async prepareResolutionLearningStep(access, input) {
437
- permission(access);
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", [