@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.
- package/README.md +46 -22
- 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 +8 -0
- package/dist/engineering-followup-source.d.ts.map +1 -1
- package/dist/engineering-followup-source.js +96 -18
- package/dist/engineering-followup-source.js.map +1 -1
- package/dist/engineering-followup-types.d.ts +45 -3
- package/dist/engineering-followup-types.d.ts.map +1 -1
- package/dist/engineering-followup-work.d.ts +5 -5
- package/dist/engineering-followup-work.d.ts.map +1 -1
- package/dist/engineering-followup-work.js +471 -107
- 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 +49 -2
- 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
|
@@ -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
|
|
261
|
-
|
|
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
|
|
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
|
-
|
|
334
|
-
|
|
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
|
|
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
|
-
|
|
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, "
|
|
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
|
-
"
|
|
704
|
+
"stepId",
|
|
705
|
+
"requestHash",
|
|
481
706
|
]));
|
|
482
707
|
validateFence(command);
|
|
483
|
-
|
|
484
|
-
|
|
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
|
|
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
|
|
494
|
-
|
|
495
|
-
if (
|
|
496
|
-
work.
|
|
497
|
-
work.
|
|
498
|
-
work.
|
|
499
|
-
work.
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
-
...
|
|
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
|
-
|
|
560
|
-
requestHash,
|
|
561
|
-
binding,
|
|
745
|
+
callState: "possibly_called",
|
|
562
746
|
accounting: "unknown",
|
|
563
|
-
},
|
|
564
|
-
],
|
|
747
|
+
}),
|
|
565
748
|
};
|
|
566
|
-
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
...
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1197
|
-
const stored = await
|
|
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
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
proof.
|
|
1216
|
-
proof.context
|
|
1217
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|