@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
@@ -2,7 +2,7 @@ import { engineeringMutationBoundary, } from "./engineering-mutation-guard.js";
2
2
  import { SupportDeskConflictError, SupportDeskNotFoundError, supportPermissions, supportTicketAuditActions, } from "./index.js";
3
3
  import { canonical, fields, fingerprint, integer, text, timestamp, } from "./assistant-turn-records.js";
4
4
  import { safeEngineeringFollowupSnapshot as snapshot, checkEngineeringFollowupRecordCapacity, reserveEngineeringFollowupReachable, engineeringFollowupWorkKey, engineeringFollowupCommandKey, validateEngineeringFollowupCandidate, validateEngineeringFollowupBundle, validateEngineeringFollowupModelBinding, engineeringFollowupModelManifestHash, engineeringFollowupContextHash, engineeringFollowupPublicFactsHash, engineeringFollowupChecksBinding, engineeringFollowupRetestProductBinding, engineeringFollowupRetestRequestHash, engineeringFollowupRetestRequestId, engineeringFollowupRetestSummary, engineeringFollowupModelAttemptCount, } from "./engineering-followup-records.js";
5
- import { createEngineeringFollowupSourceServices, engineeringFollowupPermission as permission, engineeringFollowupObservation, engineeringFollowupNow, engineeringFollowupTrusted, engineeringFollowupResult as result, engineeringFollowupWithoutLease as withoutLease, engineeringFollowupCurrent, engineeringFollowupVersionsCurrent, holdEngineeringFollowupWork, engineeringFollowupAssociation, engineeringFollowupProofContext, engineeringFollowupValidateProof, planEngineeringFollowupContextChange, } from "./engineering-followup-source.js";
5
+ import { createEngineeringFollowupSourceServices, engineeringFollowupPermission as permission, engineeringFollowupObservation, engineeringFollowupNow, engineeringFollowupTrusted, engineeringFollowupResult as result, engineeringFollowupWithoutLease as withoutLease, engineeringFollowupCurrent, engineeringFollowupActive, engineeringFollowupLoadMutation, engineeringFollowupVersionsCurrent, holdEngineeringFollowupWork, engineeringFollowupAssociation, engineeringFollowupProofContext, engineeringFollowupValidateProof, planEngineeringFollowupContextChange, } from "./engineering-followup-source.js";
6
6
  import { engineeringSourceKey } from "./engineering-records.js";
7
7
  function validateFence(command) {
8
8
  for (const value of [
@@ -81,9 +81,9 @@ export function createEngineeringFollowupWorkServices(deps, load) {
81
81
  if (mutation !== undefined) {
82
82
  const assertHostCurrent = await mutation.verify();
83
83
  assertHostCurrent();
84
- beforeCommit?.();
85
- mutation.ensure();
86
84
  }
85
+ beforeCommit?.();
86
+ mutation?.ensure();
87
87
  return (await records.transact(next.ticketId, [
88
88
  {
89
89
  action: "putIfUnchanged",
@@ -257,8 +257,14 @@ export function createEngineeringFollowupWorkServices(deps, load) {
257
257
  if (typeof guard !== "function")
258
258
  throw new TypeError("Follow-up bundle preparation needs a current read-only guard");
259
259
  mutation?.ensure();
260
- const loaded = initial?.current.loaded ?? (await load(command.ticketId));
261
- const stored = initial?.current.work ?? (await workAt(command));
260
+ const { loaded, stored } = initial !== undefined
261
+ ? { loaded: initial.current.loaded, stored: initial.current.work }
262
+ : staff
263
+ ? {
264
+ loaded: await load(command.ticketId),
265
+ stored: await workAt(command),
266
+ }
267
+ : await engineeringFollowupLoadMutation(deps, command);
262
268
  const work = stored.value;
263
269
  if (work.workGeneration !== command.expectedWorkGeneration ||
264
270
  work.candidateHash !== command.candidateHash ||
@@ -275,7 +281,7 @@ export function createEngineeringFollowupWorkServices(deps, load) {
275
281
  : null;
276
282
  const current = staff
277
283
  ? null
278
- : await engineeringFollowupCurrent(deps, loaded, stored, now);
284
+ : await engineeringFollowupActive(deps, loaded, stored, now);
279
285
  if (!staff &&
280
286
  (work.stage !== "bundle" ||
281
287
  work.candidate === undefined ||
@@ -330,8 +336,14 @@ export function createEngineeringFollowupWorkServices(deps, load) {
330
336
  excludedWorkId: work.workId,
331
337
  });
332
338
  deps.admission(loaded.ticket.value.ticket, loaded.rows, [next, ...cohort.nextWorkRows], 2);
333
- await guard();
334
- const proof = rearm?.proof ?? current.proof;
339
+ // Source/association admission precedes receipt callbacks. The release
340
+ // proof is deliberately fresh AFTER verifyBundle, at the late guard's
341
+ // original release-observation boundary rather than reused across it.
342
+ const proof = rearm?.proof ?? (await lateBundleProof(stored, current, now));
343
+ if (staff)
344
+ await guard();
345
+ else
346
+ await bundleGuard(access, work, proof, guard, authorize, now);
335
347
  const associationWork = rearm?.engineeringWork ?? current.engineeringWork;
336
348
  if (!(await engineeringFollowupVersionsCurrent(deps, loaded, stored, associationWork))) {
337
349
  if (initial !== undefined)
@@ -367,6 +379,212 @@ export function createEngineeringFollowupWorkServices(deps, load) {
367
379
  throw new ReassessmentRetry();
368
380
  throw new SupportDeskConflictError();
369
381
  }
382
+ async function lateBundleProof(stored, current, now) {
383
+ if (options.releaseEvidence === undefined)
384
+ throw new SupportDeskConflictError("Follow-up release verification unavailable");
385
+ const proof = snapshot(await engineeringFollowupTrusted(deps, (signal) => options.releaseEvidence.confirm(stored.value.context, current.proofContext, signal)));
386
+ engineeringFollowupValidateProof(proof, current.proofContext, current.engineeringWork.value.product, now);
387
+ if (canonical(proof.context) !== canonical(stored.value.context) ||
388
+ proof.context.mapping === undefined ||
389
+ proof.context.deploymentHead.state !== "deployed" ||
390
+ timestamp(stored.value.deadline, "deadline") <= now())
391
+ throw new SupportDeskConflictError("Follow-up deployment changed");
392
+ return proof;
393
+ }
394
+ async function bundleGuard(access, work, proof, guard, authorize, now) {
395
+ const actorId = access.principalId;
396
+ let active = true;
397
+ const check = (reader) => {
398
+ if (!active)
399
+ throw new SupportDeskConflictError("Follow-up observation expired");
400
+ authorize();
401
+ permission(reader, supportPermissions.engineeringFollowupRead, actorId, true);
402
+ if (now() >=
403
+ Math.min(timestamp(proof.attestation.validUntil, "validUntil"), timestamp(work.deadline, "deadline")))
404
+ throw new SupportDeskConflictError("Follow-up bundle observation expired");
405
+ };
406
+ function detached(value) {
407
+ const copy = snapshot(value);
408
+ function freeze(item) {
409
+ if (item === null || typeof item !== "object")
410
+ return;
411
+ for (const child of Object.values(item))
412
+ freeze(child);
413
+ Object.freeze(item);
414
+ }
415
+ freeze(copy);
416
+ return copy;
417
+ }
418
+ const observation = Object.freeze({
419
+ readWork(reader) {
420
+ check(reader);
421
+ return detached(work);
422
+ },
423
+ readProof(reader) {
424
+ check(reader);
425
+ return detached(proof);
426
+ },
427
+ });
428
+ try {
429
+ await guard(observation);
430
+ }
431
+ finally {
432
+ active = false;
433
+ }
434
+ }
435
+ async function modelGuard(access, work, proof, guard) {
436
+ if (typeof guard !== "function")
437
+ throw new TypeError("Follow-up invocation needs a current guard");
438
+ const actorId = access.principalId;
439
+ let active = true;
440
+ const check = (current) => {
441
+ if (!active)
442
+ throw new SupportDeskConflictError("Follow-up observation expired");
443
+ permission(current, supportPermissions.engineeringFollowupRead, actorId);
444
+ permission(current, supportPermissions.queueRead, actorId);
445
+ };
446
+ function freeze(value) {
447
+ if (value === null || typeof value !== "object")
448
+ return;
449
+ for (const item of Object.values(value))
450
+ freeze(item);
451
+ Object.freeze(value);
452
+ }
453
+ try {
454
+ await guard(Object.freeze({
455
+ readWork(current) {
456
+ check(current);
457
+ return snapshot(work);
458
+ },
459
+ readProof(current) {
460
+ check(current);
461
+ const detached = snapshot(proof);
462
+ freeze(detached);
463
+ return detached;
464
+ },
465
+ }));
466
+ }
467
+ finally {
468
+ active = false;
469
+ }
470
+ }
471
+ async function prepareModel(access, input, staged = false, guard) {
472
+ const actorId = access.principalId;
473
+ const authorize = () => permission(access, supportPermissions.engineeringFollowupProcess, actorId);
474
+ authorize();
475
+ const command = snapshot(fields(input, "prepare follow-up model", [
476
+ "ticketId",
477
+ "workId",
478
+ "workerId",
479
+ "claimId",
480
+ "claimGeneration",
481
+ "binding",
482
+ ]));
483
+ validateFence(command);
484
+ validateEngineeringFollowupModelBinding(command.binding);
485
+ if ((command.binding.staged !== undefined) !== staged)
486
+ throw new TypeError("Follow-up preparation mode differs from binding");
487
+ const now = engineeringFollowupNow(deps);
488
+ if (!options.enabled)
489
+ throw new SupportDeskConflictError("Follow-up model processing disabled");
490
+ for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
491
+ const { loaded, stored } = await engineeringFollowupLoadMutation(deps, command);
492
+ const work = stored.value;
493
+ live(work, command, now());
494
+ const current = await engineeringFollowupCurrent(deps, loaded, stored, now);
495
+ const binding = command.binding;
496
+ const manifest = binding.inputManifest;
497
+ if (work.stage !== "model" ||
498
+ work.candidate !== undefined ||
499
+ work.preparedBundle !== undefined ||
500
+ work.completionRows !== 5 ||
501
+ work.messageSlots !== 1 ||
502
+ manifest.workBindingHash !== work.workBindingHash ||
503
+ manifest.contextHash !==
504
+ (await engineeringFollowupContextHash(work.context)) ||
505
+ manifest.publicFactsHash !==
506
+ (await engineeringFollowupPublicFactsHash(current.proof.publicFacts)) ||
507
+ binding.inputManifestHash !==
508
+ (await engineeringFollowupModelManifestHash(manifest)) ||
509
+ manifest.messages.some((reference) => !loaded.rows.some((row) => row.kind === "message" &&
510
+ row.message.visibility === "customer" &&
511
+ row.message.id === reference.messageId &&
512
+ row.ordinal === reference.ordinal &&
513
+ row.ordinal <= work.acceptedTicketRevision)))
514
+ throw new SupportDeskConflictError("Follow-up model manifest differs from current facts");
515
+ const deadline = timestamp(binding.deadline, "model.deadline");
516
+ if (deadline <= now() ||
517
+ deadline > timestamp(work.deadline, "deadline") ||
518
+ (!staged && deadline > timestamp(work.leaseUntil, "leaseUntil")))
519
+ throw new SupportDeskConflictError("Follow-up model deadline exceeds live claim");
520
+ const existing = work.modelAttempts.find((entry) => canonical(entry.binding) === canonical(binding));
521
+ if (existing !== undefined) {
522
+ if (existing.preparedClaimGeneration !== command.claimGeneration ||
523
+ existing.stepId !== work.stepId)
524
+ throw new SupportDeskConflictError("Follow-up preparation belongs to another physical invocation");
525
+ if (staged)
526
+ await modelGuard(access, work, current.proof, guard);
527
+ if (!(await engineeringFollowupVersionsCurrent(deps, loaded, stored, current.engineeringWork)))
528
+ continue;
529
+ authorize();
530
+ live(work, command, now());
531
+ if (now() >=
532
+ Math.min(deadline, timestamp(current.proof.attestation.validUntil, "validUntil")))
533
+ throw new SupportDeskConflictError("Follow-up model proof expired");
534
+ if (await save(loaded, stored, work)) {
535
+ authorize();
536
+ return {
537
+ stepId: existing.stepId,
538
+ requestHash: existing.requestHash,
539
+ maxCandidateBytes: 16_000,
540
+ };
541
+ }
542
+ continue;
543
+ }
544
+ if (engineeringFollowupModelAttemptCount(work) >=
545
+ options.maxModelAttempts ||
546
+ work.modelAttempts.some((entry) => entry.preparedClaimGeneration === command.claimGeneration))
547
+ throw new SupportDeskConflictError("Follow-up paid attempt bound reached");
548
+ const requestHash = await fingerprint(binding);
549
+ const stepId = await fingerprint({
550
+ workId: work.workId,
551
+ claimGeneration: command.claimGeneration,
552
+ binding,
553
+ });
554
+ const next = {
555
+ ...compactModelAttempts(staged ? withoutLease(work) : work),
556
+ status: staged ? "retry" : "model_prepared",
557
+ stepId,
558
+ requestHash,
559
+ workGeneration: work.workGeneration + 1,
560
+ modelAttempts: [
561
+ {
562
+ preparedClaimGeneration: command.claimGeneration,
563
+ stepId,
564
+ requestHash,
565
+ binding,
566
+ accounting: staged ? "not_called" : "unknown",
567
+ ...(staged ? { callState: "not_called" } : {}),
568
+ },
569
+ ],
570
+ };
571
+ reserveEngineeringFollowupReachable(next);
572
+ if (staged)
573
+ await modelGuard(access, work, current.proof, guard);
574
+ if (!(await engineeringFollowupVersionsCurrent(deps, loaded, stored, current.engineeringWork)))
575
+ continue;
576
+ authorize();
577
+ live(work, command, now());
578
+ if (now() >=
579
+ Math.min(deadline, timestamp(current.proof.attestation.validUntil, "validUntil")))
580
+ throw new SupportDeskConflictError("Follow-up model proof expired");
581
+ if (await save(loaded, stored, next)) {
582
+ authorize();
583
+ return { stepId, requestHash, maxCandidateBytes: 16_000 };
584
+ }
585
+ }
586
+ throw new SupportDeskConflictError();
587
+ }
370
588
  const application = {
371
589
  async claimEngineeringFollowupWork(access, input) {
372
590
  const actorId = access.principalId;
@@ -391,8 +609,7 @@ export function createEngineeringFollowupWorkServices(deps, load) {
391
609
  return null;
392
610
  const now = engineeringFollowupNow(deps);
393
611
  for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
394
- const loaded = await load(command.ticketId);
395
- const stored = await workAt(command);
612
+ const { loaded, stored } = await engineeringFollowupLoadMutation(deps, command);
396
613
  const work = stored.value;
397
614
  if ([
398
615
  "held",
@@ -450,6 +667,7 @@ export function createEngineeringFollowupWorkServices(deps, load) {
450
667
  if (await save(loaded, stored, next)) {
451
668
  authorize();
452
669
  return {
670
+ bundleObservation: true,
453
671
  fence: snapshot(next.claim),
454
672
  sourceFence: {
455
673
  ticketId: work.ticketId,
@@ -467,113 +685,173 @@ export function createEngineeringFollowupWorkServices(deps, load) {
467
685
  }
468
686
  throw new SupportDeskConflictError();
469
687
  },
470
- async prepareEngineeringFollowupModel(access, input) {
688
+ prepareEngineeringFollowupModel(access, input) {
689
+ return prepareModel(access, input);
690
+ },
691
+ planEngineeringFollowupModel(access, input, guard) {
692
+ return prepareModel(access, input, true, guard);
693
+ },
694
+ async beginEngineeringFollowupModel(access, input, guard) {
471
695
  const actorId = access.principalId;
472
696
  const authorize = () => permission(access, supportPermissions.engineeringFollowupProcess, actorId);
473
697
  authorize();
474
- const command = snapshot(fields(input, "prepare follow-up model", [
698
+ const command = snapshot(fields(input, "begin follow-up model", [
475
699
  "ticketId",
476
700
  "workId",
477
701
  "workerId",
478
702
  "claimId",
479
703
  "claimGeneration",
480
- "binding",
704
+ "stepId",
705
+ "requestHash",
481
706
  ]));
482
707
  validateFence(command);
483
- validateEngineeringFollowupModelBinding(command.binding);
484
- const now = engineeringFollowupNow(deps);
708
+ text(command.stepId, "stepId");
709
+ text(command.requestHash, "requestHash");
485
710
  if (!options.enabled)
486
711
  throw new SupportDeskConflictError("Follow-up model processing disabled");
712
+ const now = engineeringFollowupNow(deps);
487
713
  for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
488
- const loaded = await load(command.ticketId);
489
- const stored = await workAt(command);
714
+ const { loaded, stored } = await engineeringFollowupLoadMutation(deps, command);
490
715
  const work = stored.value;
491
716
  live(work, command, now());
492
717
  const current = await engineeringFollowupCurrent(deps, loaded, stored, now);
493
- const binding = command.binding;
494
- const manifest = binding.inputManifest;
495
- if (work.stage !== "model" ||
496
- work.candidate !== undefined ||
497
- work.preparedBundle !== undefined ||
498
- work.completionRows !== 5 ||
499
- work.messageSlots !== 1 ||
500
- manifest.workBindingHash !== work.workBindingHash ||
501
- manifest.contextHash !==
502
- (await engineeringFollowupContextHash(work.context)) ||
503
- manifest.publicFactsHash !==
504
- (await engineeringFollowupPublicFactsHash(current.proof.publicFacts)) ||
505
- binding.inputManifestHash !==
506
- (await engineeringFollowupModelManifestHash(manifest)) ||
507
- manifest.messages.some((reference) => !loaded.rows.some((row) => row.kind === "message" &&
508
- row.message.visibility === "customer" &&
509
- row.message.id === reference.messageId &&
510
- row.ordinal === reference.ordinal &&
511
- row.ordinal <= work.acceptedTicketRevision)))
512
- throw new SupportDeskConflictError("Follow-up model manifest differs from current facts");
513
- const deadline = timestamp(binding.deadline, "model.deadline");
514
- if (deadline <= now() ||
515
- deadline > timestamp(work.deadline, "deadline") ||
516
- deadline > timestamp(work.leaseUntil, "leaseUntil"))
517
- throw new SupportDeskConflictError("Follow-up model deadline exceeds live claim");
518
- const existing = work.modelAttempts.find((entry) => canonical(entry.binding) === canonical(binding));
519
- if (existing !== undefined) {
520
- if (existing.preparedClaimGeneration !== command.claimGeneration ||
521
- existing.stepId !== work.stepId)
522
- throw new SupportDeskConflictError("Follow-up preparation belongs to another physical invocation");
523
- if (!(await engineeringFollowupVersionsCurrent(deps, loaded, stored, current.engineeringWork)))
524
- continue;
718
+ const model = work.modelAttempts.find((entry) => entry.stepId === command.stepId &&
719
+ entry.requestHash === command.requestHash);
720
+ if (model?.binding.staged === undefined ||
721
+ work.stepId !== command.stepId ||
722
+ work.requestHash !== command.requestHash ||
723
+ work.stage !== "model" ||
724
+ work.pendingModelCompletion !== undefined)
725
+ throw new SupportDeskConflictError("Follow-up model plan changed");
726
+ if (model.callState !== "not_called" ||
727
+ model.accounting !== "not_called") {
525
728
  authorize();
526
- live(work, command, now());
527
- if (now() >=
528
- Math.min(deadline, timestamp(current.proof.attestation.validUntil, "validUntil")))
529
- throw new SupportDeskConflictError("Follow-up model proof expired");
530
- if (await save(loaded, stored, work)) {
531
- authorize();
532
- return {
533
- stepId: existing.stepId,
534
- requestHash: existing.requestHash,
535
- maxCandidateBytes: 16_000,
536
- };
537
- }
538
- continue;
729
+ return {
730
+ stepId: model.stepId,
731
+ requestHash: model.requestHash,
732
+ maxCandidateBytes: 16_000,
733
+ callPermit: false,
734
+ };
539
735
  }
540
- if (engineeringFollowupModelAttemptCount(work) >=
541
- options.maxModelAttempts ||
542
- work.modelAttempts.some((entry) => entry.preparedClaimGeneration === command.claimGeneration))
543
- throw new SupportDeskConflictError("Follow-up paid attempt bound reached");
544
- const requestHash = await fingerprint(binding);
545
- const stepId = await fingerprint({
546
- workId: work.workId,
547
- claimGeneration: command.claimGeneration,
548
- binding,
549
- });
550
736
  const next = {
551
- ...compactModelAttempts(work),
737
+ ...work,
552
738
  status: "model_prepared",
553
- stepId,
554
- requestHash,
555
739
  workGeneration: work.workGeneration + 1,
556
- modelAttempts: [
557
- {
740
+ modelAttempts: work.modelAttempts.map((entry) => entry !== model
741
+ ? entry
742
+ : {
743
+ ...entry,
558
744
  preparedClaimGeneration: command.claimGeneration,
559
- stepId,
560
- requestHash,
561
- binding,
745
+ callState: "possibly_called",
562
746
  accounting: "unknown",
563
- },
564
- ],
747
+ }),
565
748
  };
566
- reserveEngineeringFollowupReachable(next);
749
+ checkEngineeringFollowupRecordCapacity(next);
750
+ await modelGuard(access, work, current.proof, guard);
567
751
  if (!(await engineeringFollowupVersionsCurrent(deps, loaded, stored, current.engineeringWork)))
568
752
  continue;
569
753
  authorize();
570
754
  live(work, command, now());
571
755
  if (now() >=
572
- Math.min(deadline, timestamp(current.proof.attestation.validUntil, "validUntil")))
573
- throw new SupportDeskConflictError("Follow-up model proof expired");
756
+ Math.min(timestamp(model.binding.deadline, "model.deadline"), timestamp(current.proof.attestation.validUntil, "validUntil")))
757
+ throw new SupportDeskConflictError("Follow-up model plan expired");
574
758
  if (await save(loaded, stored, next)) {
575
759
  authorize();
576
- return { stepId, requestHash, maxCandidateBytes: 16_000 };
760
+ return {
761
+ stepId: model.stepId,
762
+ requestHash: model.requestHash,
763
+ maxCandidateBytes: 16_000,
764
+ callPermit: true,
765
+ };
766
+ }
767
+ }
768
+ throw new SupportDeskConflictError();
769
+ },
770
+ async recordEngineeringFollowupModel(access, input, guard) {
771
+ const actorId = access.principalId;
772
+ const authorize = () => permission(access, supportPermissions.engineeringFollowupProcess, actorId);
773
+ authorize();
774
+ const command = snapshot(fields(input, "record follow-up model", [
775
+ "ticketId",
776
+ "workId",
777
+ "workerId",
778
+ "claimId",
779
+ "claimGeneration",
780
+ "stepId",
781
+ "requestHash",
782
+ "candidate",
783
+ "accounting",
784
+ ], ["processing"]));
785
+ validateFence(command);
786
+ text(command.stepId, "stepId");
787
+ text(command.requestHash, "requestHash");
788
+ validateEngineeringFollowupCandidate(command.candidate);
789
+ processing(command.processing);
790
+ if (!["accounted", "unknown"].includes(command.accounting))
791
+ throw new TypeError("Invalid follow-up accounting");
792
+ const hash = await fingerprint({ actorId, command });
793
+ const now = engineeringFollowupNow(deps);
794
+ for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
795
+ const { loaded, stored } = await engineeringFollowupLoadMutation(deps, command);
796
+ const work = stored.value;
797
+ if (work.lastModelRecord?.fingerprint === hash) {
798
+ authorize();
799
+ return snapshot(work.lastModelRecord.result);
800
+ }
801
+ if (!options.enabled)
802
+ throw new SupportDeskConflictError("Follow-up model processing disabled");
803
+ live(work, command, now());
804
+ const current = await engineeringFollowupCurrent(deps, loaded, stored, now);
805
+ const model = work.modelAttempts.find((entry) => entry.stepId === command.stepId &&
806
+ entry.requestHash === command.requestHash);
807
+ if (model?.binding.staged === undefined ||
808
+ model.callState !== "possibly_called" ||
809
+ model.preparedClaimGeneration !== command.claimGeneration ||
810
+ work.stepId !== command.stepId ||
811
+ work.requestHash !== command.requestHash ||
812
+ work.stage !== "model" ||
813
+ work.pendingModelCompletion !== undefined)
814
+ throw new SupportDeskConflictError("Follow-up model invocation changed");
815
+ const next = {
816
+ ...withoutLease(work),
817
+ status: "retry",
818
+ workGeneration: work.workGeneration + 1,
819
+ pendingModelCompletion: {
820
+ stepId: command.stepId,
821
+ requestHash: command.requestHash,
822
+ candidate: command.candidate,
823
+ accounting: command.accounting,
824
+ ...(command.processing === undefined
825
+ ? {}
826
+ : { processing: command.processing }),
827
+ },
828
+ modelAttempts: work.modelAttempts.map((entry) => entry !== model
829
+ ? entry
830
+ : {
831
+ ...entry,
832
+ callState: "recorded",
833
+ accounting: command.accounting,
834
+ ...(command.processing === undefined
835
+ ? {}
836
+ : { processing: command.processing }),
837
+ }),
838
+ };
839
+ const accepted = {
840
+ ...next,
841
+ lastModelRecord: { fingerprint: hash, result: result(next) },
842
+ };
843
+ checkEngineeringFollowupRecordCapacity(accepted);
844
+ await modelGuard(access, work, current.proof, guard);
845
+ if (!(await engineeringFollowupVersionsCurrent(deps, loaded, stored, current.engineeringWork)))
846
+ continue;
847
+ authorize();
848
+ live(work, command, now());
849
+ if (now() >=
850
+ Math.min(timestamp(model.binding.deadline, "model.deadline"), timestamp(current.proof.attestation.validUntil, "validUntil")))
851
+ throw new SupportDeskConflictError("Follow-up model result expired");
852
+ if (await save(loaded, stored, accepted)) {
853
+ authorize();
854
+ return result(next);
577
855
  }
578
856
  }
579
857
  throw new SupportDeskConflictError();
@@ -603,8 +881,7 @@ export function createEngineeringFollowupWorkServices(deps, load) {
603
881
  const hash = await fingerprint({ actorId, command });
604
882
  const now = engineeringFollowupNow(deps);
605
883
  for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
606
- const loaded = await load(command.ticketId);
607
- const stored = await workAt(command);
884
+ const { loaded, stored } = await engineeringFollowupLoadMutation(deps, command);
608
885
  const work = stored.value;
609
886
  if (work.lastCompletion?.fingerprint === hash) {
610
887
  authorize();
@@ -616,12 +893,27 @@ export function createEngineeringFollowupWorkServices(deps, load) {
616
893
  entry.requestHash === command.requestHash);
617
894
  if (work.stage !== "model" ||
618
895
  model === undefined ||
619
- model.preparedClaimGeneration !== command.claimGeneration ||
896
+ (model.preparedClaimGeneration !== command.claimGeneration &&
897
+ work.pendingModelCompletion === undefined) ||
620
898
  work.stepId !== command.stepId ||
621
899
  work.requestHash !== command.requestHash)
622
900
  throw new SupportDeskConflictError("Follow-up prepared model owner changed");
901
+ if (model.binding.staged !== undefined &&
902
+ (model.callState !== "recorded" ||
903
+ canonical(work.pendingModelCompletion) !==
904
+ canonical({
905
+ stepId: command.stepId,
906
+ requestHash: command.requestHash,
907
+ candidate: command.candidate,
908
+ accounting: command.accounting,
909
+ ...(command.processing === undefined
910
+ ? {}
911
+ : { processing: command.processing }),
912
+ })))
913
+ throw new SupportDeskConflictError("Follow-up recorded model result changed");
914
+ const { pendingModelCompletion: _pendingCompletion, ...retained } = withoutLease(work);
623
915
  const next = compactModelAttempts({
624
- ...withoutLease(work),
916
+ ...retained,
625
917
  status: "candidate",
626
918
  stage: "bundle",
627
919
  candidate: command.candidate,
@@ -642,7 +934,10 @@ export function createEngineeringFollowupWorkServices(deps, load) {
642
934
  lastCompletion: { fingerprint: hash, result: result(next) },
643
935
  };
644
936
  checkEngineeringFollowupRecordCapacity(accepted);
645
- await guard();
937
+ if (model.binding.staged === undefined)
938
+ await guard();
939
+ else
940
+ await modelGuard(access, work, current.proof, guard);
646
941
  if (!(await engineeringFollowupVersionsCurrent(deps, loaded, stored, current.engineeringWork)))
647
942
  continue;
648
943
  authorize();
@@ -703,8 +998,7 @@ export function createEngineeringFollowupWorkServices(deps, load) {
703
998
  throw new TypeError("Historical restriction cannot change accounting");
704
999
  const hash = await fingerprint({ actorId, command });
705
1000
  for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
706
- const loaded = await load(command.ticketId);
707
- const stored = await workAt(command);
1001
+ const { loaded, stored } = await engineeringFollowupLoadMutation(deps, command);
708
1002
  const work = stored.value;
709
1003
  if (work.lastFinish?.fingerprint === hash) {
710
1004
  authorize();
@@ -769,6 +1063,10 @@ export function createEngineeringFollowupWorkServices(deps, load) {
769
1063
  throw new SupportDeskConflictError("Cannot rewrite another follow-up invocation's accounting");
770
1064
  return entry;
771
1065
  }
1066
+ if (entry.callState !== undefined &&
1067
+ entry.callState !== "not_called" &&
1068
+ command.accounting === "not_called")
1069
+ throw new SupportDeskConflictError("A possibly called follow-up cannot become not-called");
772
1070
  if (entry.accounting !== "unknown" &&
773
1071
  command.accounting !== undefined &&
774
1072
  entry.accounting !== command.accounting)
@@ -1009,7 +1307,7 @@ export function createEngineeringFollowupWorkServices(deps, load) {
1009
1307
  if (work.workGeneration !== command.expectedWorkGeneration ||
1010
1308
  engineeringFollowupModelAttemptCount(work) >= options.maxModelAttempts)
1011
1309
  throw new SupportDeskConflictError("Follow-up recovery generation or lifetime model budget changed");
1012
- const { stepId: _step, requestHash: _request, heldReason: _reason, ...retained } = withoutLease(compactModelAttempts(work));
1310
+ const { stepId: _step, requestHash: _request, pendingModelCompletion: _pendingCompletion, heldReason: _reason, ...retained } = withoutLease(compactModelAttempts(work));
1013
1311
  const next = {
1014
1312
  ...retained,
1015
1313
  ...current.prospective,
@@ -1162,10 +1460,11 @@ export function createEngineeringFollowupWorkServices(deps, load) {
1162
1460
  }
1163
1461
  throw new SupportDeskConflictError();
1164
1462
  },
1165
- async submitEngineeringFollowupRetestResult(access, input) {
1463
+ async submitEngineeringFollowupRetestResult(access, input, control) {
1166
1464
  const actorId = access.principalId;
1167
1465
  const authorize = () => permission(access, supportPermissions.engineeringFollowupEvidenceSubmit, actorId);
1168
1466
  authorize();
1467
+ const mutation = engineeringMutationBoundary(control);
1169
1468
  const command = snapshot(fields(input, "submit follow-up retest", [
1170
1469
  "ticketId",
1171
1470
  "workId",
@@ -1193,8 +1492,8 @@ export function createEngineeringFollowupWorkServices(deps, load) {
1193
1492
  throw new SupportDeskConflictError("Wrong retest receipt kind");
1194
1493
  return previous;
1195
1494
  }
1196
- const loaded = await load(command.ticketId);
1197
- const stored = await workAt(command);
1495
+ mutation?.ensure();
1496
+ const { loaded, stored } = await engineeringFollowupLoadMutation(deps, command);
1198
1497
  const work = stored.value;
1199
1498
  if (work.withdrawn ||
1200
1499
  work.security !== "ordinary" ||
@@ -1209,12 +1508,17 @@ export function createEngineeringFollowupWorkServices(deps, load) {
1209
1508
  if (options.releaseEvidence === undefined)
1210
1509
  throw new SupportDeskConflictError("Follow-up release verification unavailable");
1211
1510
  authorize();
1212
- const proof = snapshot(await engineeringFollowupTrusted(deps, (signal) => options.releaseEvidence.confirm(work.context, proofContext, signal)));
1213
- engineeringFollowupValidateProof(proof, proofContext, associated.engineeringWork.value.product, now);
1214
- if (canonical(proof.context) !== canonical(work.context) ||
1215
- proof.context.mapping === undefined ||
1216
- proof.context.deploymentHead.state !== "deployed")
1217
- throw new SupportDeskConflictError("Follow-up retest deployment changed");
1511
+ const observer = options.retestReceipts.observe;
1512
+ const currentReleaseProof = async () => {
1513
+ const proof = snapshot(await engineeringFollowupTrusted(deps, (signal) => options.releaseEvidence.confirm(work.context, proofContext, signal)));
1514
+ engineeringFollowupValidateProof(proof, proofContext, associated.engineeringWork.value.product, now);
1515
+ if (canonical(proof.context) !== canonical(work.context) ||
1516
+ proof.context.mapping === undefined ||
1517
+ proof.context.deploymentHead.state !== "deployed")
1518
+ throw new SupportDeskConflictError("Follow-up retest deployment changed");
1519
+ return proof;
1520
+ };
1521
+ const legacyProof = observer === undefined ? await currentReleaseProof() : undefined;
1218
1522
  const requests = work.preparedBundle?.bundle.retestRequests ??
1219
1523
  work.handoff?.retestRequests;
1220
1524
  const request = requests?.find((entry) => entry.requestId === command.retestRequestId);
@@ -1231,7 +1535,60 @@ export function createEngineeringFollowupWorkServices(deps, load) {
1231
1535
  productBinding: request.productBinding,
1232
1536
  };
1233
1537
  authorize();
1234
- const verified = snapshot(await engineeringFollowupTrusted(deps, (signal) => options.retestReceipts.verify(command.receiptReference, binding, signal)));
1538
+ const opened = await Promise.allSettled([
1539
+ legacyProof === undefined
1540
+ ? currentReleaseProof()
1541
+ : Promise.resolve(legacyProof),
1542
+ engineeringFollowupTrusted(deps, async (signal) => {
1543
+ if (observer === undefined)
1544
+ return {
1545
+ verified: snapshot(await options.retestReceipts.verify(command.receiptReference, binding, signal)),
1546
+ confirm: undefined,
1547
+ };
1548
+ const observed = await observer.call(options.retestReceipts, command.receiptReference, binding, signal);
1549
+ if (observed === null)
1550
+ return { verified: null, confirm: undefined };
1551
+ // Snapshot immediately, before any later awaited work or confirmation.
1552
+ const verified = snapshot(observed.verified);
1553
+ if (typeof observed.confirm !== "function")
1554
+ throw new TypeError("Follow-up retest observation lacks confirmation");
1555
+ return { verified, confirm: observed.confirm.bind(observed) };
1556
+ }),
1557
+ ]);
1558
+ if (opened[0].status === "rejected")
1559
+ throw opened[0].reason;
1560
+ const proof = snapshot(opened[0].value);
1561
+ if (opened[1].status === "rejected")
1562
+ throw opened[1].reason;
1563
+ const observation = opened[1].value;
1564
+ const verified = observation.verified;
1565
+ let assertRetest;
1566
+ let confirmed = observer === undefined;
1567
+ const confirmCurrent = async () => {
1568
+ const closing = await Promise.allSettled([
1569
+ engineeringFollowupVersionsCurrent(deps, loaded, stored, associated.engineeringWork),
1570
+ observation.confirm === undefined
1571
+ ? Promise.resolve(undefined)
1572
+ : engineeringFollowupTrusted(deps, (signal) => observation.confirm(signal)),
1573
+ ]);
1574
+ if (closing[0].status === "rejected")
1575
+ throw closing[0].reason;
1576
+ if (closing[1].status === "rejected")
1577
+ throw closing[1].reason;
1578
+ const assertion = closing[1].value;
1579
+ if (assertion !== undefined && typeof assertion !== "function")
1580
+ throw new TypeError("Invalid closing retest assertion");
1581
+ assertRetest =
1582
+ typeof assertion === "function" ? assertion : undefined;
1583
+ confirmed =
1584
+ observer === undefined || observation.confirm !== undefined;
1585
+ return closing[0].value;
1586
+ };
1587
+ const assertConfirmed = () => {
1588
+ if (!confirmed ||
1589
+ (assertRetest !== undefined && assertRetest() !== true))
1590
+ throw new SupportDeskConflictError("Follow-up retest confirmation changed");
1591
+ };
1235
1592
  if (verified === null ||
1236
1593
  canonical(verified.binding) !== canonical(binding) ||
1237
1594
  !["passed", "failed"].includes(verified.result) ||
@@ -1248,12 +1605,13 @@ export function createEngineeringFollowupWorkServices(deps, load) {
1248
1605
  const summary = await engineeringFollowupRetestSummary(verified, command.receiptReference);
1249
1606
  const prior = work.retestResults.find((entry) => entry.requestId === request.requestId);
1250
1607
  if (prior?.verificationHash === summary.verificationHash) {
1251
- if (!(await engineeringFollowupVersionsCurrent(deps, loaded, stored, associated.engineeringWork)))
1608
+ if (!(await confirmCurrent()))
1252
1609
  continue;
1253
1610
  authorize();
1254
1611
  if (now() >=
1255
1612
  Math.min(timestamp(verified.validUntil, "validUntil"), timestamp(proof.attestation.validUntil, "validUntil")))
1256
1613
  throw new SupportDeskConflictError("Follow-up retest proof expired");
1614
+ assertConfirmed();
1257
1615
  return {
1258
1616
  ticketId: work.ticketId,
1259
1617
  workId: work.workId,
@@ -1300,7 +1658,7 @@ export function createEngineeringFollowupWorkServices(deps, load) {
1300
1658
  : "recorded",
1301
1659
  result: verified.result,
1302
1660
  };
1303
- if (!(await engineeringFollowupVersionsCurrent(deps, loaded, stored, associated.engineeringWork)))
1661
+ if (!(await confirmCurrent()))
1304
1662
  continue;
1305
1663
  authorize();
1306
1664
  if (now() >=
@@ -1314,7 +1672,13 @@ export function createEngineeringFollowupWorkServices(deps, load) {
1314
1672
  })),
1315
1673
  deps.audit(command, loaded.ticket.value.ticket, actorId, clock.now(), supportTicketAuditActions.engineeringFollowupRetestRecorded),
1316
1674
  receipt(command, hash, outcome),
1317
- ])) {
1675
+ ], loaded.head?.value, mutation, () => {
1676
+ authorize();
1677
+ assertConfirmed();
1678
+ if (now() >=
1679
+ Math.min(timestamp(verified.validUntil, "validUntil"), timestamp(proof.attestation.validUntil, "validUntil")))
1680
+ throw new SupportDeskConflictError("Follow-up retest proof expired");
1681
+ })) {
1318
1682
  authorize();
1319
1683
  return outcome;
1320
1684
  }