@pegma/support-desk-application 0.2.7 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +46 -20
  2. package/dist/engineering-followup-internal.d.ts +9 -2
  3. package/dist/engineering-followup-internal.d.ts.map +1 -1
  4. package/dist/engineering-followup-records.d.ts +12 -0
  5. package/dist/engineering-followup-records.d.ts.map +1 -1
  6. package/dist/engineering-followup-records.js +55 -5
  7. package/dist/engineering-followup-records.js.map +1 -1
  8. package/dist/engineering-followup-source.d.ts +18 -7
  9. package/dist/engineering-followup-source.d.ts.map +1 -1
  10. package/dist/engineering-followup-source.js +191 -23
  11. package/dist/engineering-followup-source.js.map +1 -1
  12. package/dist/engineering-followup-types.d.ts +75 -9
  13. package/dist/engineering-followup-types.d.ts.map +1 -1
  14. package/dist/engineering-followup-work.d.ts +9 -12
  15. package/dist/engineering-followup-work.d.ts.map +1 -1
  16. package/dist/engineering-followup-work.js +783 -234
  17. package/dist/engineering-followup-work.js.map +1 -1
  18. package/dist/engineering-handoff.d.ts +6 -0
  19. package/dist/engineering-handoff.d.ts.map +1 -0
  20. package/dist/engineering-handoff.js +31 -0
  21. package/dist/engineering-handoff.js.map +1 -0
  22. package/dist/engineering-types.d.ts +28 -0
  23. package/dist/engineering-types.d.ts.map +1 -1
  24. package/dist/engineering-work.d.ts +1 -1
  25. package/dist/engineering-work.d.ts.map +1 -1
  26. package/dist/engineering-work.js +534 -32
  27. package/dist/engineering-work.js.map +1 -1
  28. package/dist/index.d.ts +13 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +65 -4
  31. package/dist/index.js.map +1 -1
  32. package/dist/resolution-learning-delivery-handoff.d.ts +5 -0
  33. package/dist/resolution-learning-delivery-handoff.d.ts.map +1 -0
  34. package/dist/resolution-learning-delivery-handoff.js +20 -0
  35. package/dist/resolution-learning-delivery-handoff.js.map +1 -0
  36. package/dist/resolution-learning-types.d.ts +42 -1
  37. package/dist/resolution-learning-types.d.ts.map +1 -1
  38. package/dist/resolution-learning-worker.d.ts +1 -1
  39. package/dist/resolution-learning-worker.d.ts.map +1 -1
  40. package/dist/resolution-learning-worker.js +594 -369
  41. package/dist/resolution-learning-worker.js.map +1 -1
  42. package/package.json +3 -3
@@ -1,7 +1,9 @@
1
+ import { engineeringMutationBoundary, } from "./engineering-mutation-guard.js";
1
2
  import { SupportDeskConflictError, SupportDeskNotFoundError, supportPermissions, supportTicketAuditActions, } from "./index.js";
2
3
  import { canonical, fields, fingerprint, integer, text, timestamp, } from "./assistant-turn-records.js";
3
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";
4
- import { createEngineeringFollowupSourceServices, engineeringFollowupPermission as permission, 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
+ import { engineeringSourceKey } from "./engineering-records.js";
5
7
  function validateFence(command) {
6
8
  for (const value of [
7
9
  command.ticketId,
@@ -71,11 +73,17 @@ export function createEngineeringFollowupWorkServices(deps, load) {
71
73
  throw new SupportDeskNotFoundError();
72
74
  return { value: work.value, version: work.version };
73
75
  }
74
- async function save(loaded, work, next, extra = [], nextHead = loaded.head?.value) {
76
+ async function save(loaded, work, next, extra = [], nextHead = loaded.head?.value, mutation, beforeCommit) {
75
77
  if (loaded.head === null || nextHead === undefined)
76
78
  throw new SupportDeskConflictError("Follow-up head disappeared");
77
79
  checkEngineeringFollowupRecordCapacity(next);
78
80
  checkEngineeringFollowupRecordCapacity(nextHead);
81
+ if (mutation !== undefined) {
82
+ const assertHostCurrent = await mutation.verify();
83
+ assertHostCurrent();
84
+ }
85
+ beforeCommit?.();
86
+ mutation?.ensure();
79
87
  return (await records.transact(next.ticketId, [
80
88
  {
81
89
  action: "putIfUnchanged",
@@ -188,6 +196,395 @@ export function createEngineeringFollowupWorkServices(deps, load) {
188
196
  text(checked.verifierVersion, "verifierVersion");
189
197
  return { binding, deadline: timestamp(checked.validUntil, "validUntil") };
190
198
  }
199
+ class ReassessmentRetry extends Error {
200
+ }
201
+ async function prepareBundle(access, input, guard, control, initial) {
202
+ const actorId = access.principalId;
203
+ const command = snapshot(fields(input, "prepare follow-up bundle", [
204
+ "ticketId",
205
+ "workId",
206
+ "commandId",
207
+ "correlationId",
208
+ "mode",
209
+ "expectedWorkGeneration",
210
+ "candidateHash",
211
+ "exactBundle",
212
+ "checksReceipt",
213
+ "checkPolicyVersion",
214
+ ], ["recoveryIntentHash"]));
215
+ if (command.mode !== "prepare" && command.mode !== "staff_reassessment")
216
+ throw new TypeError("Invalid follow-up preparation mode");
217
+ const staff = command.mode === "staff_reassessment";
218
+ const authorize = () => {
219
+ initial?.authorize();
220
+ permission(access, staff
221
+ ? supportPermissions.engineeringFollowupReview
222
+ : supportPermissions.engineeringFollowupProcess, actorId, staff);
223
+ };
224
+ authorize();
225
+ const mutation = initial?.mutation ?? engineeringMutationBoundary(control);
226
+ for (const entry of [
227
+ command.ticketId,
228
+ command.workId,
229
+ command.commandId,
230
+ command.correlationId,
231
+ command.candidateHash,
232
+ command.checksReceipt,
233
+ command.checkPolicyVersion,
234
+ ])
235
+ text(entry, "bundle preparation identity");
236
+ integer(command.expectedWorkGeneration, "expectedWorkGeneration", 1);
237
+ validateEngineeringFollowupBundle(command.exactBundle);
238
+ if (command.recoveryIntentHash !== undefined &&
239
+ (!staff || !/^[a-f0-9]{64}$/.test(command.recoveryIntentHash)))
240
+ throw new TypeError("Invalid preparation recovery intent");
241
+ const hash = await fingerprint({ actorId, command });
242
+ const recoveryFingerprint = command.recoveryIntentHash === undefined
243
+ ? undefined
244
+ : await fingerprint({
245
+ actorId,
246
+ intentHash: command.recoveryIntentHash,
247
+ });
248
+ const now = engineeringFollowupNow(deps);
249
+ for (let attempt = 0; attempt < (initial === undefined ? deps.maxConflictAttempts : 1); attempt += 1) {
250
+ const previous = await replay(command, hash);
251
+ authorize();
252
+ if (previous !== null) {
253
+ if (!("status" in previous))
254
+ throw new SupportDeskConflictError("Wrong follow-up receipt kind");
255
+ return previous;
256
+ }
257
+ if (typeof guard !== "function")
258
+ throw new TypeError("Follow-up bundle preparation needs a current read-only guard");
259
+ mutation?.ensure();
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);
268
+ const work = stored.value;
269
+ if (work.workGeneration !== command.expectedWorkGeneration ||
270
+ work.candidateHash !== command.candidateHash ||
271
+ work.withdrawn ||
272
+ work.status === "delivered" ||
273
+ work.security !== "ordinary")
274
+ throw new SupportDeskConflictError("Follow-up bundle source, candidate or generation changed");
275
+ if (!staff && timestamp(work.deadline, "deadline") <= now())
276
+ throw new SupportDeskConflictError("Expired follow-up requires explicit staff reassessment");
277
+ if (work.bundleRevision >= options.maxBundleRevisions)
278
+ throw new SupportDeskConflictError("Follow-up bundle preparation bound reached");
279
+ const rearm = staff
280
+ ? await sourceServices.internal.reassessment(command, now, "bundle", initial?.current, initial === undefined ? undefined : () => initial.authorize())
281
+ : null;
282
+ const current = staff
283
+ ? null
284
+ : await engineeringFollowupActive(deps, loaded, stored, now);
285
+ if (!staff &&
286
+ (work.stage !== "bundle" ||
287
+ work.candidate === undefined ||
288
+ work.completionRows !== 5 ||
289
+ work.messageSlots !== 1))
290
+ throw new SupportDeskConflictError("Follow-up bundle preparation needs its current model candidate");
291
+ if (rearm !== null &&
292
+ (rearm.work.version !== stored.version ||
293
+ rearm.loaded.ticket.version !== loaded.ticket.version ||
294
+ rearm.loaded.source.version !== loaded.source.version ||
295
+ rearm.loaded.head?.version !== loaded.head?.version)) {
296
+ if (initial !== undefined)
297
+ throw new ReassessmentRetry();
298
+ continue;
299
+ }
300
+ const base = rearm === null
301
+ ? work
302
+ : {
303
+ ...work,
304
+ ...rearm.prospective,
305
+ workBindingHash: rearm.prospectiveWorkBindingHash,
306
+ };
307
+ const checked = await verifyBundle(base, command.exactBundle, command.candidateHash, command.checkPolicyVersion, command.checksReceipt, now);
308
+ const { candidate: _candidate, ...retained } = withoutLease(base);
309
+ const next = {
310
+ ...retained,
311
+ stage: "bundle",
312
+ status: "bundle_prepared",
313
+ completionRows: 3,
314
+ messageSlots: 1,
315
+ deadline: staff
316
+ ? new Date(now() + options.maxWorkMilliseconds).toISOString()
317
+ : work.deadline,
318
+ workGeneration: work.workGeneration + 1,
319
+ bundleRevision: work.bundleRevision + 1,
320
+ preparedBundle: {
321
+ bundle: command.exactBundle,
322
+ bundleHash: checked.binding.bundleHash,
323
+ candidateHash: command.candidateHash,
324
+ binding: checked.binding,
325
+ checksReceipt: command.checksReceipt,
326
+ },
327
+ };
328
+ reserveEngineeringFollowupReachable(next);
329
+ const cohort = rearm === null
330
+ ? { actions: [], nextWorkRows: [] }
331
+ : await planEngineeringFollowupContextChange(records, {
332
+ rows: loaded.rows,
333
+ context: rearm.proof.context,
334
+ previousHead: loaded.head.value,
335
+ nextHead: rearm.nextHead,
336
+ excludedWorkId: work.workId,
337
+ });
338
+ deps.admission(loaded.ticket.value.ticket, loaded.rows, [next, ...cohort.nextWorkRows], 2);
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);
347
+ const associationWork = rearm?.engineeringWork ?? current.engineeringWork;
348
+ if (!(await engineeringFollowupVersionsCurrent(deps, loaded, stored, associationWork))) {
349
+ if (initial !== undefined)
350
+ throw new ReassessmentRetry();
351
+ continue;
352
+ }
353
+ authorize();
354
+ if (!staff && timestamp(work.deadline, "deadline") <= now())
355
+ throw new SupportDeskConflictError("Expired follow-up requires explicit staff reassessment");
356
+ if (now() >=
357
+ Math.min(checked.deadline, timestamp(proof.attestation.validUntil, "validUntil"), timestamp(next.deadline, "deadline")))
358
+ throw new SupportDeskConflictError("Follow-up bundle proof expired");
359
+ const outcome = result(next);
360
+ if (await save(loaded, stored, next, [
361
+ ...cohort.actions,
362
+ deps.audit(command, loaded.ticket.value.ticket, actorId, clock.now(), staff
363
+ ? supportTicketAuditActions.engineeringFollowupReassessed
364
+ : supportTicketAuditActions.engineeringFollowupBundlePrepared),
365
+ receipt(command, hash, outcome, recoveryFingerprint),
366
+ ], rearm?.nextHead ?? loaded.head.value, mutation, () => {
367
+ authorize();
368
+ if (!staff && timestamp(work.deadline, "deadline") <= now())
369
+ throw new SupportDeskConflictError("Expired follow-up requires explicit staff reassessment");
370
+ if (now() >=
371
+ Math.min(checked.deadline, timestamp(proof.attestation.validUntil, "validUntil"), timestamp(next.deadline, "deadline")))
372
+ throw new SupportDeskConflictError("Follow-up bundle proof expired");
373
+ })) {
374
+ authorize();
375
+ return outcome;
376
+ }
377
+ }
378
+ if (initial !== undefined)
379
+ throw new ReassessmentRetry();
380
+ throw new SupportDeskConflictError();
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
+ }
191
588
  const application = {
192
589
  async claimEngineeringFollowupWork(access, input) {
193
590
  const actorId = access.principalId;
@@ -212,8 +609,7 @@ export function createEngineeringFollowupWorkServices(deps, load) {
212
609
  return null;
213
610
  const now = engineeringFollowupNow(deps);
214
611
  for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
215
- const loaded = await load(command.ticketId);
216
- const stored = await workAt(command);
612
+ const { loaded, stored } = await engineeringFollowupLoadMutation(deps, command);
217
613
  const work = stored.value;
218
614
  if ([
219
615
  "held",
@@ -271,6 +667,7 @@ export function createEngineeringFollowupWorkServices(deps, load) {
271
667
  if (await save(loaded, stored, next)) {
272
668
  authorize();
273
669
  return {
670
+ bundleObservation: true,
274
671
  fence: snapshot(next.claim),
275
672
  sourceFence: {
276
673
  ticketId: work.ticketId,
@@ -288,113 +685,173 @@ export function createEngineeringFollowupWorkServices(deps, load) {
288
685
  }
289
686
  throw new SupportDeskConflictError();
290
687
  },
291
- 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) {
292
695
  const actorId = access.principalId;
293
696
  const authorize = () => permission(access, supportPermissions.engineeringFollowupProcess, actorId);
294
697
  authorize();
295
- const command = snapshot(fields(input, "prepare follow-up model", [
698
+ const command = snapshot(fields(input, "begin follow-up model", [
296
699
  "ticketId",
297
700
  "workId",
298
701
  "workerId",
299
702
  "claimId",
300
703
  "claimGeneration",
301
- "binding",
704
+ "stepId",
705
+ "requestHash",
302
706
  ]));
303
707
  validateFence(command);
304
- validateEngineeringFollowupModelBinding(command.binding);
305
- const now = engineeringFollowupNow(deps);
708
+ text(command.stepId, "stepId");
709
+ text(command.requestHash, "requestHash");
306
710
  if (!options.enabled)
307
711
  throw new SupportDeskConflictError("Follow-up model processing disabled");
712
+ const now = engineeringFollowupNow(deps);
308
713
  for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
309
- const loaded = await load(command.ticketId);
310
- const stored = await workAt(command);
714
+ const { loaded, stored } = await engineeringFollowupLoadMutation(deps, command);
311
715
  const work = stored.value;
312
716
  live(work, command, now());
313
717
  const current = await engineeringFollowupCurrent(deps, loaded, stored, now);
314
- const binding = command.binding;
315
- const manifest = binding.inputManifest;
316
- if (work.stage !== "model" ||
317
- work.candidate !== undefined ||
318
- work.preparedBundle !== undefined ||
319
- work.completionRows !== 5 ||
320
- work.messageSlots !== 1 ||
321
- manifest.workBindingHash !== work.workBindingHash ||
322
- manifest.contextHash !==
323
- (await engineeringFollowupContextHash(work.context)) ||
324
- manifest.publicFactsHash !==
325
- (await engineeringFollowupPublicFactsHash(current.proof.publicFacts)) ||
326
- binding.inputManifestHash !==
327
- (await engineeringFollowupModelManifestHash(manifest)) ||
328
- manifest.messages.some((reference) => !loaded.rows.some((row) => row.kind === "message" &&
329
- row.message.visibility === "customer" &&
330
- row.message.id === reference.messageId &&
331
- row.ordinal === reference.ordinal &&
332
- row.ordinal <= work.acceptedTicketRevision)))
333
- throw new SupportDeskConflictError("Follow-up model manifest differs from current facts");
334
- const deadline = timestamp(binding.deadline, "model.deadline");
335
- if (deadline <= now() ||
336
- deadline > timestamp(work.deadline, "deadline") ||
337
- deadline > timestamp(work.leaseUntil, "leaseUntil"))
338
- throw new SupportDeskConflictError("Follow-up model deadline exceeds live claim");
339
- const existing = work.modelAttempts.find((entry) => canonical(entry.binding) === canonical(binding));
340
- if (existing !== undefined) {
341
- if (existing.preparedClaimGeneration !== command.claimGeneration ||
342
- existing.stepId !== work.stepId)
343
- throw new SupportDeskConflictError("Follow-up preparation belongs to another physical invocation");
344
- if (!(await engineeringFollowupVersionsCurrent(deps, loaded, stored, current.engineeringWork)))
345
- 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") {
346
728
  authorize();
347
- live(work, command, now());
348
- if (now() >=
349
- Math.min(deadline, timestamp(current.proof.attestation.validUntil, "validUntil")))
350
- throw new SupportDeskConflictError("Follow-up model proof expired");
351
- if (await save(loaded, stored, work)) {
352
- authorize();
353
- return {
354
- stepId: existing.stepId,
355
- requestHash: existing.requestHash,
356
- maxCandidateBytes: 16_000,
357
- };
358
- }
359
- continue;
729
+ return {
730
+ stepId: model.stepId,
731
+ requestHash: model.requestHash,
732
+ maxCandidateBytes: 16_000,
733
+ callPermit: false,
734
+ };
360
735
  }
361
- if (engineeringFollowupModelAttemptCount(work) >=
362
- options.maxModelAttempts ||
363
- work.modelAttempts.some((entry) => entry.preparedClaimGeneration === command.claimGeneration))
364
- throw new SupportDeskConflictError("Follow-up paid attempt bound reached");
365
- const requestHash = await fingerprint(binding);
366
- const stepId = await fingerprint({
367
- workId: work.workId,
368
- claimGeneration: command.claimGeneration,
369
- binding,
370
- });
371
736
  const next = {
372
- ...compactModelAttempts(work),
737
+ ...work,
373
738
  status: "model_prepared",
374
- stepId,
375
- requestHash,
376
739
  workGeneration: work.workGeneration + 1,
377
- modelAttempts: [
378
- {
740
+ modelAttempts: work.modelAttempts.map((entry) => entry !== model
741
+ ? entry
742
+ : {
743
+ ...entry,
379
744
  preparedClaimGeneration: command.claimGeneration,
380
- stepId,
381
- requestHash,
382
- binding,
745
+ callState: "possibly_called",
383
746
  accounting: "unknown",
384
- },
385
- ],
747
+ }),
386
748
  };
387
- reserveEngineeringFollowupReachable(next);
749
+ checkEngineeringFollowupRecordCapacity(next);
750
+ await modelGuard(access, work, current.proof, guard);
388
751
  if (!(await engineeringFollowupVersionsCurrent(deps, loaded, stored, current.engineeringWork)))
389
752
  continue;
390
753
  authorize();
391
754
  live(work, command, now());
392
755
  if (now() >=
393
- Math.min(deadline, timestamp(current.proof.attestation.validUntil, "validUntil")))
394
- 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");
395
758
  if (await save(loaded, stored, next)) {
396
759
  authorize();
397
- 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);
398
855
  }
399
856
  }
400
857
  throw new SupportDeskConflictError();
@@ -424,8 +881,7 @@ export function createEngineeringFollowupWorkServices(deps, load) {
424
881
  const hash = await fingerprint({ actorId, command });
425
882
  const now = engineeringFollowupNow(deps);
426
883
  for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
427
- const loaded = await load(command.ticketId);
428
- const stored = await workAt(command);
884
+ const { loaded, stored } = await engineeringFollowupLoadMutation(deps, command);
429
885
  const work = stored.value;
430
886
  if (work.lastCompletion?.fingerprint === hash) {
431
887
  authorize();
@@ -437,12 +893,27 @@ export function createEngineeringFollowupWorkServices(deps, load) {
437
893
  entry.requestHash === command.requestHash);
438
894
  if (work.stage !== "model" ||
439
895
  model === undefined ||
440
- model.preparedClaimGeneration !== command.claimGeneration ||
896
+ (model.preparedClaimGeneration !== command.claimGeneration &&
897
+ work.pendingModelCompletion === undefined) ||
441
898
  work.stepId !== command.stepId ||
442
899
  work.requestHash !== command.requestHash)
443
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);
444
915
  const next = compactModelAttempts({
445
- ...withoutLease(work),
916
+ ...retained,
446
917
  status: "candidate",
447
918
  stage: "bundle",
448
919
  candidate: command.candidate,
@@ -463,7 +934,10 @@ export function createEngineeringFollowupWorkServices(deps, load) {
463
934
  lastCompletion: { fingerprint: hash, result: result(next) },
464
935
  };
465
936
  checkEngineeringFollowupRecordCapacity(accepted);
466
- await guard();
937
+ if (model.binding.staged === undefined)
938
+ await guard();
939
+ else
940
+ await modelGuard(access, work, current.proof, guard);
467
941
  if (!(await engineeringFollowupVersionsCurrent(deps, loaded, stored, current.engineeringWork)))
468
942
  continue;
469
943
  authorize();
@@ -524,8 +998,7 @@ export function createEngineeringFollowupWorkServices(deps, load) {
524
998
  throw new TypeError("Historical restriction cannot change accounting");
525
999
  const hash = await fingerprint({ actorId, command });
526
1000
  for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
527
- const loaded = await load(command.ticketId);
528
- const stored = await workAt(command);
1001
+ const { loaded, stored } = await engineeringFollowupLoadMutation(deps, command);
529
1002
  const work = stored.value;
530
1003
  if (work.lastFinish?.fingerprint === hash) {
531
1004
  authorize();
@@ -590,6 +1063,10 @@ export function createEngineeringFollowupWorkServices(deps, load) {
590
1063
  throw new SupportDeskConflictError("Cannot rewrite another follow-up invocation's accounting");
591
1064
  return entry;
592
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");
593
1070
  if (entry.accounting !== "unknown" &&
594
1071
  command.accounting !== undefined &&
595
1072
  entry.accounting !== command.accounting)
@@ -649,159 +1126,155 @@ export function createEngineeringFollowupWorkServices(deps, load) {
649
1126
  throw new SupportDeskConflictError("Follow-up recovery identity does not match accepted preparation");
650
1127
  return snapshot(prior.result);
651
1128
  },
652
- async prepareEngineeringFollowupBundle(access, input, guard) {
1129
+ async prepareEngineeringFollowupBundle(access, input, guard, control) {
1130
+ return prepareBundle(access, input, guard, control);
1131
+ },
1132
+ async reassessEngineeringFollowupBundle(access, input, planner, control) {
653
1133
  const actorId = access.principalId;
654
- const command = snapshot(fields(input, "prepare follow-up bundle", [
1134
+ const authorize = (reader = access) => {
1135
+ permission(reader, supportPermissions.engineeringFollowupReview, actorId, true);
1136
+ permission(reader, supportPermissions.engineeringFollowupRead, actorId, true);
1137
+ };
1138
+ permission(access, supportPermissions.engineeringFollowupReview, actorId, true);
1139
+ const command = snapshot(fields(input, "reassess follow-up bundle", [
655
1140
  "ticketId",
656
1141
  "workId",
657
1142
  "commandId",
658
1143
  "correlationId",
659
- "mode",
660
1144
  "expectedWorkGeneration",
661
- "candidateHash",
662
- "exactBundle",
663
- "checksReceipt",
664
- "checkPolicyVersion",
665
- ], ["recoveryIntentHash"]));
666
- if (command.mode !== "prepare" && command.mode !== "staff_reassessment")
667
- throw new TypeError("Invalid follow-up preparation mode");
668
- const staff = command.mode === "staff_reassessment";
669
- const authorize = () => permission(access, staff
670
- ? supportPermissions.engineeringFollowupReview
671
- : supportPermissions.engineeringFollowupProcess, actorId, staff);
672
- authorize();
1145
+ "recoveryIntentHash",
1146
+ ]));
673
1147
  for (const entry of [
674
1148
  command.ticketId,
675
1149
  command.workId,
676
1150
  command.commandId,
677
1151
  command.correlationId,
678
- command.candidateHash,
679
- command.checksReceipt,
680
- command.checkPolicyVersion,
681
1152
  ])
682
- text(entry, "bundle preparation identity");
1153
+ text(entry, "reassessment identity");
683
1154
  integer(command.expectedWorkGeneration, "expectedWorkGeneration", 1);
684
- validateEngineeringFollowupBundle(command.exactBundle);
685
- if (command.recoveryIntentHash !== undefined &&
686
- (!staff || !/^[a-f0-9]{64}$/.test(command.recoveryIntentHash)))
687
- throw new TypeError("Invalid preparation recovery intent");
688
- const hash = await fingerprint({ actorId, command });
689
- const recoveryFingerprint = command.recoveryIntentHash === undefined
690
- ? undefined
691
- : await fingerprint({
692
- actorId,
693
- intentHash: command.recoveryIntentHash,
694
- });
1155
+ if (!/^[a-f0-9]{64}$/.test(command.recoveryIntentHash))
1156
+ throw new TypeError("Invalid reassessment recovery intent");
1157
+ const recovery = {
1158
+ ticketId: command.ticketId,
1159
+ workId: command.workId,
1160
+ commandId: command.commandId,
1161
+ recoveryIntentHash: command.recoveryIntentHash,
1162
+ };
1163
+ const recover = () => application.recoverEngineeringFollowupPreparation(access, recovery);
1164
+ const mutation = engineeringMutationBoundary(control);
695
1165
  const now = engineeringFollowupNow(deps);
696
1166
  for (let attempt = 0; attempt < deps.maxConflictAttempts; attempt += 1) {
697
- const previous = await replay(command, hash);
1167
+ const accepted = await recover();
1168
+ if (accepted !== null)
1169
+ return accepted;
1170
+ mutation?.ensure();
698
1171
  authorize();
699
- if (previous !== null) {
700
- if (!("status" in previous))
701
- throw new SupportDeskConflictError("Wrong follow-up receipt kind");
702
- return previous;
703
- }
704
- if (typeof guard !== "function")
705
- throw new TypeError("Follow-up bundle preparation needs a current read-only guard");
706
- const loaded = await load(command.ticketId);
707
- const stored = await workAt(command);
708
- const work = stored.value;
709
- if (work.workGeneration !== command.expectedWorkGeneration ||
710
- work.candidateHash !== command.candidateHash ||
711
- work.withdrawn ||
712
- work.status === "delivered" ||
713
- work.security !== "ordinary")
714
- throw new SupportDeskConflictError("Follow-up bundle source, candidate or generation changed");
715
- if (!staff && timestamp(work.deadline, "deadline") <= now())
716
- throw new SupportDeskConflictError("Expired follow-up requires explicit staff reassessment");
717
- if (work.bundleRevision >= options.maxBundleRevisions)
718
- throw new SupportDeskConflictError("Follow-up bundle preparation bound reached");
719
- const rearm = staff
720
- ? await sourceServices.internal.reassessment(command, now)
721
- : null;
722
- const current = staff
723
- ? null
724
- : await engineeringFollowupCurrent(deps, loaded, stored, now);
725
- if (!staff &&
726
- (work.stage !== "bundle" ||
727
- work.candidate === undefined ||
728
- work.completionRows !== 5 ||
729
- work.messageSlots !== 1))
730
- throw new SupportDeskConflictError("Follow-up bundle preparation needs its current model candidate");
731
- if (rearm !== null &&
732
- (rearm.work.version !== stored.version ||
733
- rearm.loaded.ticket.version !== loaded.ticket.version ||
734
- rearm.loaded.source.version !== loaded.source.version ||
735
- rearm.loaded.head?.version !== loaded.head?.version))
736
- continue;
737
- const base = rearm === null
738
- ? work
739
- : {
740
- ...work,
741
- ...rearm.prospective,
742
- workBindingHash: rearm.prospectiveWorkBindingHash,
743
- };
744
- const checked = await verifyBundle(base, command.exactBundle, command.candidateHash, command.checkPolicyVersion, command.checksReceipt, now);
745
- const { candidate: _candidate, ...retained } = withoutLease(base);
746
- const next = {
747
- ...retained,
748
- stage: "bundle",
749
- status: "bundle_prepared",
750
- completionRows: 3,
751
- messageSlots: 1,
752
- deadline: staff
753
- ? new Date(now() + options.maxWorkMilliseconds).toISOString()
754
- : work.deadline,
755
- workGeneration: work.workGeneration + 1,
756
- bundleRevision: work.bundleRevision + 1,
757
- preparedBundle: {
758
- bundle: command.exactBundle,
759
- bundleHash: checked.binding.bundleHash,
760
- candidateHash: command.candidateHash,
761
- binding: checked.binding,
762
- checksReceipt: command.checksReceipt,
763
- },
764
- };
765
- reserveEngineeringFollowupReachable(next);
766
- const cohort = rearm === null
767
- ? { actions: [], nextWorkRows: [] }
768
- : await planEngineeringFollowupContextChange(records, {
769
- rows: loaded.rows,
770
- context: rearm.proof.context,
771
- previousHead: loaded.head.value,
772
- nextHead: rearm.nextHead,
773
- excludedWorkId: work.workId,
774
- });
775
- deps.admission(loaded.ticket.value.ticket, loaded.rows, [next, ...cohort.nextWorkRows], 2);
776
- await guard();
777
- const proof = rearm?.proof ?? current.proof;
778
- const associationWork = rearm?.engineeringWork ?? current.engineeringWork;
779
- if (!(await engineeringFollowupVersionsCurrent(deps, loaded, stored, associationWork)))
780
- continue;
1172
+ // Eligibility and private Source authority precede any protected hydration.
1173
+ const source = await records.getVersioned(engineeringSourceKey(command.ticketId));
781
1174
  authorize();
782
- if (!staff && timestamp(work.deadline, "deadline") <= now())
783
- throw new SupportDeskConflictError("Expired follow-up requires explicit staff reassessment");
784
- if (now() >=
785
- Math.min(checked.deadline, timestamp(proof.attestation.validUntil, "validUntil"), timestamp(next.deadline, "deadline")))
786
- throw new SupportDeskConflictError("Follow-up bundle proof expired");
787
- const outcome = result(next);
788
- if (await save(loaded, stored, next, [
789
- ...cohort.actions,
790
- deps.audit(command, loaded.ticket.value.ticket, actorId, clock.now(), staff
791
- ? supportTicketAuditActions.engineeringFollowupReassessed
792
- : supportTicketAuditActions.engineeringFollowupBundlePrepared),
793
- receipt(command, hash, outcome, recoveryFingerprint),
794
- ], rearm?.nextHead ?? loaded.head.value)) {
1175
+ if (source?.value.kind !== "engineering_source")
1176
+ throw new SupportDeskNotFoundError();
1177
+ if (source.value.security !== "ordinary")
1178
+ permission(access, supportPermissions.engineeringRestrictedRead, actorId, true);
1179
+ if (source.value.security !== "ordinary" ||
1180
+ source.value.state === "withdrawn")
1181
+ throw new SupportDeskConflictError("Follow-up Source is ineligible");
1182
+ try {
1183
+ const current = await sourceServices.internal.reassessment(command, now, "bundle", undefined, (source) => {
1184
+ authorize();
1185
+ if (source !== undefined) {
1186
+ if (source.security !== "ordinary")
1187
+ permission(access, supportPermissions.engineeringRestrictedRead, actorId, true);
1188
+ if (source.security !== "ordinary" ||
1189
+ source.state === "withdrawn")
1190
+ throw new SupportDeskConflictError("Follow-up Source is ineligible");
1191
+ }
1192
+ }, { value: source.value, version: source.version });
1193
+ // The initial external release proof cannot make the original Source current.
1194
+ const confirmedSource = await records.getVersioned(engineeringSourceKey(command.ticketId));
795
1195
  authorize();
796
- return outcome;
1196
+ if (confirmedSource?.value.kind !== "engineering_source")
1197
+ throw new SupportDeskNotFoundError();
1198
+ if (confirmedSource.value.security !== "ordinary")
1199
+ permission(access, supportPermissions.engineeringRestrictedRead, actorId, true);
1200
+ if (confirmedSource.version !== source.version)
1201
+ throw new SupportDeskConflictError("Follow-up Source changed during release verification");
1202
+ if (current.work.value.workGeneration !== command.expectedWorkGeneration)
1203
+ throw new SupportDeskConflictError("Follow-up reassessment generation changed");
1204
+ const scope = engineeringFollowupObservation(deps, current, actorId, authorize);
1205
+ const observation = Object.freeze({
1206
+ read(reader) {
1207
+ scope.check(reader);
1208
+ if (now() >=
1209
+ timestamp(current.proof.attestation.validUntil, "validUntil"))
1210
+ throw new SupportDeskConflictError("Follow-up reassessment expired");
1211
+ return snapshot({
1212
+ work: current.work.value,
1213
+ proofContext: current.proofContext,
1214
+ proof: current.proof,
1215
+ prospectiveWorkBindingHash: current.prospectiveWorkBindingHash,
1216
+ workGeneration: current.work.value.workGeneration,
1217
+ });
1218
+ },
1219
+ confirm: scope.confirm,
1220
+ });
1221
+ try {
1222
+ if (typeof planner !== "function")
1223
+ throw new TypeError("Follow-up reassessment needs a read-only planner");
1224
+ const planned = await planner(observation);
1225
+ authorize();
1226
+ const checkedPlan = fields(planned, "follow-up reassessment plan", [
1227
+ "candidateHash",
1228
+ "exactBundle",
1229
+ "checksReceipt",
1230
+ "checkPolicyVersion",
1231
+ "guard",
1232
+ ]);
1233
+ const guard = checkedPlan.guard;
1234
+ if (typeof guard !== "function")
1235
+ throw new TypeError("Follow-up reassessment guard must be callable");
1236
+ const plan = snapshot({
1237
+ candidateHash: checkedPlan.candidateHash,
1238
+ exactBundle: checkedPlan.exactBundle,
1239
+ checksReceipt: checkedPlan.checksReceipt,
1240
+ checkPolicyVersion: checkedPlan.checkPolicyVersion,
1241
+ });
1242
+ // Functions never enter the immutable command or an accepted receipt.
1243
+ return await prepareBundle(access, {
1244
+ ...command,
1245
+ mode: "staff_reassessment",
1246
+ candidateHash: plan.candidateHash,
1247
+ exactBundle: plan.exactBundle,
1248
+ checksReceipt: plan.checksReceipt,
1249
+ checkPolicyVersion: plan.checkPolicyVersion,
1250
+ }, async () => {
1251
+ try {
1252
+ return await guard();
1253
+ }
1254
+ finally {
1255
+ scope.close();
1256
+ }
1257
+ }, undefined, { current, mutation, authorize });
1258
+ }
1259
+ finally {
1260
+ scope.close();
1261
+ }
1262
+ }
1263
+ catch (error) {
1264
+ const accepted = await recover();
1265
+ if (accepted !== null)
1266
+ return accepted;
1267
+ if (!(error instanceof ReassessmentRetry))
1268
+ throw error;
797
1269
  }
798
1270
  }
799
1271
  throw new SupportDeskConflictError();
800
1272
  },
801
- async retryEngineeringFollowupWork(access, input) {
1273
+ async retryEngineeringFollowupWork(access, input, control) {
802
1274
  const actorId = access.principalId;
803
1275
  const authorize = () => permission(access, supportPermissions.engineeringFollowupReview, actorId, true);
804
1276
  authorize();
1277
+ const mutation = engineeringMutationBoundary(control);
805
1278
  const now = engineeringFollowupNow(deps);
806
1279
  const command = snapshot(fields(input, "retry follow-up work", [
807
1280
  "ticketId",
@@ -827,13 +1300,14 @@ export function createEngineeringFollowupWorkServices(deps, load) {
827
1300
  throw new SupportDeskConflictError("Wrong follow-up receipt kind");
828
1301
  return previous;
829
1302
  }
1303
+ mutation?.ensure();
830
1304
  const current = await sourceServices.internal.reassessment(command, now, "model_retry");
831
1305
  const { loaded, work: stored } = current;
832
1306
  const work = stored.value;
833
1307
  if (work.workGeneration !== command.expectedWorkGeneration ||
834
1308
  engineeringFollowupModelAttemptCount(work) >= options.maxModelAttempts)
835
1309
  throw new SupportDeskConflictError("Follow-up recovery generation or lifetime model budget changed");
836
- 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));
837
1311
  const next = {
838
1312
  ...retained,
839
1313
  ...current.prospective,
@@ -865,17 +1339,23 @@ export function createEngineeringFollowupWorkServices(deps, load) {
865
1339
  ...cohort.actions,
866
1340
  deps.audit(command, loaded.ticket.value.ticket, actorId, clock.now(), supportTicketAuditActions.engineeringFollowupRetried),
867
1341
  receipt(command, hash, outcome),
868
- ], current.nextHead)) {
1342
+ ], current.nextHead, mutation, () => {
1343
+ authorize();
1344
+ if (now() >=
1345
+ Math.min(timestamp(current.proof.attestation.validUntil, "validUntil"), timestamp(next.deadline, "deadline")))
1346
+ throw new SupportDeskConflictError("Follow-up recovery proof expired");
1347
+ })) {
869
1348
  authorize();
870
1349
  return outcome;
871
1350
  }
872
1351
  }
873
1352
  throw new SupportDeskConflictError();
874
1353
  },
875
- async abandonEngineeringFollowupWork(access, input) {
1354
+ async abandonEngineeringFollowupWork(access, input, control) {
876
1355
  const actorId = access.principalId;
877
1356
  const authorize = () => permission(access, supportPermissions.engineeringFollowupReview, actorId, true);
878
1357
  authorize();
1358
+ const mutation = engineeringMutationBoundary(control);
879
1359
  const command = snapshot(fields(input, "abandon follow-up work", [
880
1360
  "ticketId",
881
1361
  "workId",
@@ -902,6 +1382,7 @@ export function createEngineeringFollowupWorkServices(deps, load) {
902
1382
  throw new SupportDeskConflictError("Wrong follow-up receipt kind");
903
1383
  return previous;
904
1384
  }
1385
+ mutation?.ensure();
905
1386
  const loaded = await load(command.ticketId);
906
1387
  const stored = await workAt(command);
907
1388
  const work = stored.value;
@@ -916,17 +1397,18 @@ export function createEngineeringFollowupWorkServices(deps, load) {
916
1397
  if (await save(loaded, stored, next, [
917
1398
  deps.audit(command, loaded.ticket.value.ticket, actorId, clock.now(), supportTicketAuditActions.engineeringFollowupAbandoned),
918
1399
  receipt(command, hash, outcome),
919
- ])) {
1400
+ ], loaded.head?.value, mutation, authorize)) {
920
1401
  authorize();
921
1402
  return outcome;
922
1403
  }
923
1404
  }
924
1405
  throw new SupportDeskConflictError();
925
1406
  },
926
- async rejectEngineeringFollowupBundle(access, input) {
1407
+ async rejectEngineeringFollowupBundle(access, input, control) {
927
1408
  const actorId = access.principalId;
928
1409
  const authorize = () => permission(access, supportPermissions.engineeringFollowupReview, actorId, true);
929
1410
  authorize();
1411
+ const mutation = engineeringMutationBoundary(control);
930
1412
  const command = snapshot(fields(input, "reject follow-up bundle", [
931
1413
  "ticketId",
932
1414
  "workId",
@@ -955,6 +1437,7 @@ export function createEngineeringFollowupWorkServices(deps, load) {
955
1437
  throw new SupportDeskConflictError("Wrong follow-up receipt kind");
956
1438
  return previous;
957
1439
  }
1440
+ mutation?.ensure();
958
1441
  const loaded = await load(command.ticketId);
959
1442
  const stored = await workAt(command);
960
1443
  const work = stored.value;
@@ -970,17 +1453,18 @@ export function createEngineeringFollowupWorkServices(deps, load) {
970
1453
  if (await save(loaded, stored, next, [
971
1454
  deps.audit(command, loaded.ticket.value.ticket, actorId, clock.now(), supportTicketAuditActions.engineeringFollowupRejected),
972
1455
  receipt(command, hash, outcome),
973
- ])) {
1456
+ ], loaded.head?.value, mutation, authorize)) {
974
1457
  authorize();
975
1458
  return outcome;
976
1459
  }
977
1460
  }
978
1461
  throw new SupportDeskConflictError();
979
1462
  },
980
- async submitEngineeringFollowupRetestResult(access, input) {
1463
+ async submitEngineeringFollowupRetestResult(access, input, control) {
981
1464
  const actorId = access.principalId;
982
1465
  const authorize = () => permission(access, supportPermissions.engineeringFollowupEvidenceSubmit, actorId);
983
1466
  authorize();
1467
+ const mutation = engineeringMutationBoundary(control);
984
1468
  const command = snapshot(fields(input, "submit follow-up retest", [
985
1469
  "ticketId",
986
1470
  "workId",
@@ -1008,8 +1492,8 @@ export function createEngineeringFollowupWorkServices(deps, load) {
1008
1492
  throw new SupportDeskConflictError("Wrong retest receipt kind");
1009
1493
  return previous;
1010
1494
  }
1011
- const loaded = await load(command.ticketId);
1012
- const stored = await workAt(command);
1495
+ mutation?.ensure();
1496
+ const { loaded, stored } = await engineeringFollowupLoadMutation(deps, command);
1013
1497
  const work = stored.value;
1014
1498
  if (work.withdrawn ||
1015
1499
  work.security !== "ordinary" ||
@@ -1024,12 +1508,17 @@ export function createEngineeringFollowupWorkServices(deps, load) {
1024
1508
  if (options.releaseEvidence === undefined)
1025
1509
  throw new SupportDeskConflictError("Follow-up release verification unavailable");
1026
1510
  authorize();
1027
- const proof = snapshot(await engineeringFollowupTrusted(deps, (signal) => options.releaseEvidence.confirm(work.context, proofContext, signal)));
1028
- engineeringFollowupValidateProof(proof, proofContext, associated.engineeringWork.value.product, now);
1029
- if (canonical(proof.context) !== canonical(work.context) ||
1030
- proof.context.mapping === undefined ||
1031
- proof.context.deploymentHead.state !== "deployed")
1032
- 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;
1033
1522
  const requests = work.preparedBundle?.bundle.retestRequests ??
1034
1523
  work.handoff?.retestRequests;
1035
1524
  const request = requests?.find((entry) => entry.requestId === command.retestRequestId);
@@ -1046,7 +1535,60 @@ export function createEngineeringFollowupWorkServices(deps, load) {
1046
1535
  productBinding: request.productBinding,
1047
1536
  };
1048
1537
  authorize();
1049
- 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
+ };
1050
1592
  if (verified === null ||
1051
1593
  canonical(verified.binding) !== canonical(binding) ||
1052
1594
  !["passed", "failed"].includes(verified.result) ||
@@ -1063,12 +1605,13 @@ export function createEngineeringFollowupWorkServices(deps, load) {
1063
1605
  const summary = await engineeringFollowupRetestSummary(verified, command.receiptReference);
1064
1606
  const prior = work.retestResults.find((entry) => entry.requestId === request.requestId);
1065
1607
  if (prior?.verificationHash === summary.verificationHash) {
1066
- if (!(await engineeringFollowupVersionsCurrent(deps, loaded, stored, associated.engineeringWork)))
1608
+ if (!(await confirmCurrent()))
1067
1609
  continue;
1068
1610
  authorize();
1069
1611
  if (now() >=
1070
1612
  Math.min(timestamp(verified.validUntil, "validUntil"), timestamp(proof.attestation.validUntil, "validUntil")))
1071
1613
  throw new SupportDeskConflictError("Follow-up retest proof expired");
1614
+ assertConfirmed();
1072
1615
  return {
1073
1616
  ticketId: work.ticketId,
1074
1617
  workId: work.workId,
@@ -1115,7 +1658,7 @@ export function createEngineeringFollowupWorkServices(deps, load) {
1115
1658
  : "recorded",
1116
1659
  result: verified.result,
1117
1660
  };
1118
- if (!(await engineeringFollowupVersionsCurrent(deps, loaded, stored, associated.engineeringWork)))
1661
+ if (!(await confirmCurrent()))
1119
1662
  continue;
1120
1663
  authorize();
1121
1664
  if (now() >=
@@ -1129,7 +1672,13 @@ export function createEngineeringFollowupWorkServices(deps, load) {
1129
1672
  })),
1130
1673
  deps.audit(command, loaded.ticket.value.ticket, actorId, clock.now(), supportTicketAuditActions.engineeringFollowupRetestRecorded),
1131
1674
  receipt(command, hash, outcome),
1132
- ])) {
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
+ })) {
1133
1682
  authorize();
1134
1683
  return outcome;
1135
1684
  }