@aglyn/plugins-ai 1.0.0-beta.236 → 1.0.0-beta.238

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 (164) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/ai-credits-prompt.component.d.ts +44 -0
  3. package/src/lib/components/ai-credits-prompt.component.js +117 -0
  4. package/src/lib/components/ai-credits-prompt.component.js.map +1 -0
  5. package/src/lib/components/ai-job-cancel.component.d.ts +38 -0
  6. package/src/lib/components/ai-job-cancel.component.js +156 -0
  7. package/src/lib/components/ai-job-cancel.component.js.map +1 -0
  8. package/src/lib/components/ai-job-plan.component.d.ts +5 -4
  9. package/src/lib/components/ai-job-plan.component.js +48 -12
  10. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  11. package/src/lib/components/ai-job-requests.d.ts +10 -0
  12. package/src/lib/components/ai-job-requests.js +12 -4
  13. package/src/lib/components/ai-job-requests.js.map +1 -1
  14. package/src/lib/components/ai-jobs-list-page.component.js +141 -72
  15. package/src/lib/components/ai-jobs-list-page.component.js.map +1 -1
  16. package/src/lib/components/ai-site-build-page.component.js +48 -9
  17. package/src/lib/components/ai-site-build-page.component.js.map +1 -1
  18. package/src/lib/components/ai-site-start-card.component.js +68 -22
  19. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  20. package/src/lib/components/ai-usage-strip.component.d.ts +19 -2
  21. package/src/lib/components/ai-usage-strip.component.js +40 -9
  22. package/src/lib/components/ai-usage-strip.component.js.map +1 -1
  23. package/src/lib/components/assist-build-card.component.d.ts +3 -1
  24. package/src/lib/components/assist-build-card.component.js +58 -7
  25. package/src/lib/components/assist-build-card.component.js.map +1 -1
  26. package/src/lib/components/assist-jobs-drawer.component.js +66 -22
  27. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  28. package/src/lib/components/assist-panel.component.js +89 -52
  29. package/src/lib/components/assist-panel.component.js.map +1 -1
  30. package/src/lib/jobs/ai-job-admission.d.ts +22 -2
  31. package/src/lib/jobs/ai-job-admission.js.map +1 -1
  32. package/src/lib/jobs/ai-job-build-step.d.ts +24 -11
  33. package/src/lib/jobs/ai-job-build-step.js +54 -35
  34. package/src/lib/jobs/ai-job-build-step.js.map +1 -1
  35. package/src/lib/jobs/ai-job-cancel.d.ts +71 -0
  36. package/src/lib/jobs/ai-job-cancel.js +102 -0
  37. package/src/lib/jobs/ai-job-cancel.js.map +1 -0
  38. package/src/lib/jobs/ai-job-component-step.js +4 -1
  39. package/src/lib/jobs/ai-job-component-step.js.map +1 -1
  40. package/src/lib/jobs/ai-job-drafts.js +2 -0
  41. package/src/lib/jobs/ai-job-drafts.js.map +1 -1
  42. package/src/lib/jobs/ai-job-form-step.js +4 -1
  43. package/src/lib/jobs/ai-job-form-step.js.map +1 -1
  44. package/src/lib/jobs/ai-job-layout-language.d.ts +2 -0
  45. package/src/lib/jobs/ai-job-layout-language.js +13 -1
  46. package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
  47. package/src/lib/jobs/ai-job-layout-step.js +6 -1
  48. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  49. package/src/lib/jobs/ai-job-page-language.js +12 -0
  50. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  51. package/src/lib/jobs/ai-job-page-step.d.ts +13 -0
  52. package/src/lib/jobs/ai-job-page-step.js +46 -4
  53. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  54. package/src/lib/jobs/ai-job-plan-step.d.ts +42 -0
  55. package/src/lib/jobs/ai-job-plan-step.js +118 -18
  56. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  57. package/src/lib/jobs/ai-job-site-content.d.ts +49 -3
  58. package/src/lib/jobs/ai-job-site-content.js +196 -37
  59. package/src/lib/jobs/ai-job-site-content.js.map +1 -1
  60. package/src/lib/jobs/ai-job-site-step.js +49 -16
  61. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  62. package/src/lib/jobs/ai-job-template-step.js +4 -1
  63. package/src/lib/jobs/ai-job-template-step.js.map +1 -1
  64. package/src/lib/jobs/ai-job-text-step.d.ts +8 -0
  65. package/src/lib/jobs/ai-job-text-step.js.map +1 -1
  66. package/src/lib/jobs/ai-jobs.d.ts +55 -13
  67. package/src/lib/jobs/ai-jobs.js +213 -41
  68. package/src/lib/jobs/ai-jobs.js.map +1 -1
  69. package/src/lib/jobs/ai-layout-stock-photos.d.ts +194 -34
  70. package/src/lib/jobs/ai-layout-stock-photos.js +861 -146
  71. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  72. package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.d.ts +30 -0
  73. package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.js +220 -0
  74. package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.js.map +1 -0
  75. package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.d.ts +27 -0
  76. package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.js +151 -0
  77. package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.js.map +1 -0
  78. package/src/lib/layout-language/ai-layout-compiler.d.ts +33 -0
  79. package/src/lib/layout-language/ai-layout-compiler.js +381 -18
  80. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  81. package/src/lib/layout-language/ai-layout-design.d.ts +6 -3
  82. package/src/lib/layout-language/ai-layout-design.js +5 -3
  83. package/src/lib/layout-language/ai-layout-design.js.map +1 -1
  84. package/src/lib/layout-language/ai-layout-frame.d.ts +12 -0
  85. package/src/lib/layout-language/ai-layout-frame.js +58 -3
  86. package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
  87. package/src/lib/layout-language/ai-layout-gaps.js +3 -1
  88. package/src/lib/layout-language/ai-layout-gaps.js.map +1 -1
  89. package/src/lib/layout-language/ai-layout-language.js +1 -1
  90. package/src/lib/layout-language/ai-layout-language.js.map +1 -1
  91. package/src/lib/layout-language/ai-layout-listings.d.ts +49 -1
  92. package/src/lib/layout-language/ai-layout-listings.js +117 -47
  93. package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
  94. package/src/lib/layout-language/ai-layout-pictures.d.ts +10 -1
  95. package/src/lib/layout-language/ai-layout-pictures.js +5 -2
  96. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  97. package/src/lib/layout-language/ai-layout-storefront.d.ts +82 -0
  98. package/src/lib/layout-language/ai-layout-storefront.js +223 -0
  99. package/src/lib/layout-language/ai-layout-storefront.js.map +1 -0
  100. package/src/lib/model/ai-build-job.d.ts +39 -0
  101. package/src/lib/model/ai-build-job.js +100 -1
  102. package/src/lib/model/ai-build-job.js.map +1 -1
  103. package/src/lib/model/ai-build-progress.d.ts +8 -0
  104. package/src/lib/model/ai-build-progress.js +35 -0
  105. package/src/lib/model/ai-build-progress.js.map +1 -1
  106. package/src/lib/model/ai-credit-estimate.d.ts +249 -0
  107. package/src/lib/model/ai-credit-estimate.js +278 -0
  108. package/src/lib/model/ai-credit-estimate.js.map +1 -0
  109. package/src/lib/model/ai-job-cancel-copy.d.ts +44 -0
  110. package/src/lib/model/ai-job-cancel-copy.js +53 -0
  111. package/src/lib/model/ai-job-cancel-copy.js.map +1 -0
  112. package/src/lib/model/ai-job-failure-copy.d.ts +4 -1
  113. package/src/lib/model/ai-job-failure-copy.js +8 -3
  114. package/src/lib/model/ai-job-failure-copy.js.map +1 -1
  115. package/src/lib/model/ai-jobs.types.d.ts +35 -0
  116. package/src/lib/model/ai-jobs.types.js.map +1 -1
  117. package/src/lib/model/ai-site-build-progress.js +7 -4
  118. package/src/lib/model/ai-site-build-progress.js.map +1 -1
  119. package/src/lib/model/ai-site-job.d.ts +76 -22
  120. package/src/lib/model/ai-site-job.js +125 -23
  121. package/src/lib/model/ai-site-job.js.map +1 -1
  122. package/src/lib/model/ai-site-kinds.d.ts +4 -1
  123. package/src/lib/model/ai-site-kinds.js +35 -7
  124. package/src/lib/model/ai-site-kinds.js.map +1 -1
  125. package/src/lib/runtime/ai-doctrine-validators.d.ts +37 -1
  126. package/src/lib/runtime/ai-doctrine-validators.js +161 -7
  127. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  128. package/src/lib/runtime/ai-doctrine.js +4 -3
  129. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  130. package/src/lib/runtime/ai-node-tree.d.ts +11 -0
  131. package/src/lib/runtime/ai-node-tree.js +26 -0
  132. package/src/lib/runtime/ai-node-tree.js.map +1 -1
  133. package/src/lib/runtime/ai-palette.generated.js +592 -14
  134. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  135. package/src/lib/server/ai-jobs-cancel.d.ts +13 -5
  136. package/src/lib/server/ai-jobs-cancel.js +27 -9
  137. package/src/lib/server/ai-jobs-cancel.js.map +1 -1
  138. package/src/lib/server/ai-jobs-gate.d.ts +12 -2
  139. package/src/lib/server/ai-jobs-gate.js +20 -9
  140. package/src/lib/server/ai-jobs-gate.js.map +1 -1
  141. package/src/lib/server/ai-jobs-resume.js +93 -35
  142. package/src/lib/server/ai-jobs-resume.js.map +1 -1
  143. package/src/lib/server/ai-jobs-route.js +45 -4
  144. package/src/lib/server/ai-jobs-route.js.map +1 -1
  145. package/src/lib/server/assist-chat.js +4 -1
  146. package/src/lib/server/assist-chat.js.map +1 -1
  147. package/src/lib/server/assist-edit.js +3 -1
  148. package/src/lib/server/assist-edit.js.map +1 -1
  149. package/src/lib/server/assist-music.d.ts +36 -0
  150. package/src/lib/server/assist-music.js +36 -0
  151. package/src/lib/server/assist-music.js.map +1 -0
  152. package/src/lib/server.js +6 -0
  153. package/src/lib/server.js.map +1 -1
  154. package/src/lib/usage/ai-usage-meter.d.ts +2 -1
  155. package/src/lib/usage/ai-usage-meter.js +17 -1
  156. package/src/lib/usage/ai-usage-meter.js.map +1 -1
  157. package/src/lib/usage/ai-usage-wire.d.ts +6 -0
  158. package/src/lib/usage/ai-usage-wire.js.map +1 -1
  159. package/src/lib/usage/assist-usage.d.ts +7 -0
  160. package/src/lib/usage/assist-usage.js +14 -4
  161. package/src/lib/usage/assist-usage.js.map +1 -1
  162. package/src/lib/usage/free-ai-credits-left.d.ts +7 -0
  163. package/src/lib/usage/free-ai-credits-left.js +4 -2
  164. package/src/lib/usage/free-ai-credits-left.js.map +1 -1
@@ -46,6 +46,7 @@ import { aiJobAutoConfirms } from "./ai-job-auto-confirm.js";
46
46
  import { AI_SITE_GUIDED_BUILD_FAILED_COPY, aiCustomerSafeCopy, aiCustomerSafeItem, aiCustomerSafeReview } from "../model/ai-job-failure-copy.js";
47
47
  import { aiSiteStartInputsOf } from "../model/ai-site-start.js";
48
48
  import { addAdminAudit } from "@aglyn/tenant-data-admin/server/admin-audit-write";
49
+ import { AI_JOB_CANCELED_ITEM_NOTE, aiJobCancelPending, isAiJobCanceledError, watchAiJobCancel } from "./ai-job-cancel.js";
49
50
  /**
50
51
  * AI generation jobs — the Firestore state machine (AGL-2904).
51
52
  *
@@ -383,7 +384,7 @@ function jobFrom(snapshot) {
383
384
  } : {}));
384
385
  // A job is a resource the console lists and routes to, named as one.
385
386
  const ref = jobsCollection(firestore, input.orgId).doc(createResourceUid());
386
- const data = {
387
+ const data = _extends({
387
388
  orgId: input.orgId,
388
389
  hostId: (_input_hostId = input.hostId) != null ? _input_hostId : null,
389
390
  kind: input.kind,
@@ -403,7 +404,9 @@ function jobFrom(snapshot) {
403
404
  expiresAt: assistExchangeExpiry(now),
404
405
  error: null,
405
406
  lease: null
406
- };
407
+ }, input.creditsConfirmed ? {
408
+ creditsConfirmed: input.creditsConfirmed
409
+ } : {});
407
410
  await ref.set(data);
408
411
  await logAiJobCreated(input.orgId, {
409
412
  uid: input.createdBy,
@@ -522,6 +525,8 @@ export async function getAiJob(firestore, orgId, jobId) {
522
525
  orchestration: job.orchestration
523
526
  } : {}, job.kind === 'build' && ((_job_inputs1 = job.inputs) == null ? void 0 : _job_inputs1['publish']) === true ? {
524
527
  publishAsked: true
528
+ } : {}, aiJobCancelPending(job) ? {
529
+ cancelRequested: true
525
530
  } : {});
526
531
  }
527
532
  function leaseIsLive(job, owner, nowMs) {
@@ -531,6 +536,52 @@ function leaseIsLive(job, owner, nowMs) {
531
536
  const until = toMillis(lease.until);
532
537
  return until !== null && until > nowMs;
533
538
  }
539
+ /** Whether ANY owner's lease on the job is still live: a step is in flight. */ function anyLeaseIsLive(job, nowMs) {
540
+ var _job_lease;
541
+ const until = toMillis((_job_lease = job.lease) == null ? void 0 : _job_lease.until);
542
+ return Boolean(job.lease) && until !== null && until > nowMs;
543
+ }
544
+ /**
545
+ * The write that ends a job `canceled` (AGL-3616): it holds nothing and no
546
+ * lease, a step left running goes back to `pending` (it did not finish), and
547
+ * every item not yet built is skipped with the cancel's note. What the job
548
+ * built, spent and was given back stays as it is: drafts stay drafts, and
549
+ * credits for what ran stay charged.
550
+ */ function aiJobCanceledPatch(job, now, request) {
551
+ var _job_steps;
552
+ var _job_items;
553
+ return _extends({
554
+ status: 'canceled',
555
+ cancelRequested: request,
556
+ creditsReserved: 0,
557
+ lease: null,
558
+ error: null,
559
+ steps: ((_job_steps = job.steps) != null ? _job_steps : []).map((step)=>step.status === 'running' ? _extends({}, step, {
560
+ status: 'pending',
561
+ endedAt: null
562
+ }) : step)
563
+ }, ((_job_items = job.items) == null ? void 0 : _job_items.length) ? {
564
+ items: aiCanceledItems(job.items)
565
+ } : {}, {
566
+ updatedAt: now
567
+ });
568
+ }
569
+ function aiCanceledItems(items) {
570
+ return items.map((row)=>row.status === 'pending' || row.status === 'running' ? _extends({}, row, {
571
+ status: 'skipped',
572
+ note: AI_JOB_CANCELED_ITEM_NOTE
573
+ }) : row);
574
+ }
575
+ /** The cancel a pending-cancel job carries, or one made now for a job that has none. */ function cancelRequestOf(job, now) {
576
+ var _job_cancelRequested;
577
+ return (_job_cancelRequested = job.cancelRequested) != null ? _job_cancelRequested : aiJobCancelRequest(now, null);
578
+ }
579
+ /** A cancel asked now by `by`, as the job stores it (the instant is written as a Date). */ function aiJobCancelRequest(now, by) {
580
+ return {
581
+ at: now,
582
+ by
583
+ };
584
+ }
534
585
  function nextStepIndex(job) {
535
586
  var _job_steps;
536
587
  return ((_job_steps = job.steps) != null ? _job_steps : []).findIndex((step)=>step.status === 'pending' || step.status === 'running');
@@ -559,6 +610,16 @@ function nextStepIndex(job) {
559
610
  return firestore.runTransaction(async (tx)=>{
560
611
  const job = jobFrom(await tx.get(ref));
561
612
  if (!job) return null;
613
+ // A cancel asked while a step ran, whose runner never came back to end
614
+ // it (AGL-3616): the lease ran out, so the job ends here, unclaimed.
615
+ if (aiJobCancelPending(job)) {
616
+ if (!leaseIsLive(job, owner, now.getTime())) {
617
+ tx.set(ref, aiJobCanceledPatch(job, now, cancelRequestOf(job, now)), {
618
+ merge: true
619
+ });
620
+ }
621
+ return null;
622
+ }
562
623
  const stepIndex = claimableStepIndex(job, owner, now.getTime());
563
624
  if (stepIndex === -1) return null;
564
625
  const steps = job.steps.map((step, index)=>{
@@ -630,7 +691,17 @@ function nextStepIndex(job) {
630
691
  const ref = jobsCollection(firestore, orgId).doc(jobId);
631
692
  return firestore.runTransaction(async (tx)=>{
632
693
  const job = jobFrom(await tx.get(ref));
633
- if (!job || claimableStepIndex(job, owner, now.getTime()) === -1) return null;
694
+ if (!job) return null;
695
+ // A paused workspace's job a person canceled ends canceled (AGL-3616).
696
+ if (aiJobCancelPending(job)) {
697
+ if (!leaseIsLive(job, owner, now.getTime())) {
698
+ tx.set(ref, aiJobCanceledPatch(job, now, cancelRequestOf(job, now)), {
699
+ merge: true
700
+ });
701
+ }
702
+ return null;
703
+ }
704
+ if (claimableStepIndex(job, owner, now.getTime()) === -1) return null;
634
705
  tx.set(ref, {
635
706
  updatedAt: now
636
707
  }, {
@@ -732,8 +803,8 @@ function nextStepIndex(job) {
732
803
  */ export async function recordStep(firestore, orgId, jobId, owner, stepIndex, input, now = new Date()) {
733
804
  const ref = jobsCollection(firestore, orgId).doc(jobId);
734
805
  const recorded = await firestore.runTransaction(async (tx)=>{
735
- var _ref, _input_items, _job_outputs, _input_outputs, _job_creditsSpent, _job_creditsReserved, _job_creditsReserved1, _job_refundedCredits;
736
- var _job_lease, _input_item;
806
+ var _ref, _input_items, _job_outputs, _input_outputs, _job_creditsSpent, _job_creditsReserved, _job_creditsReserved1, _job_refundedCredits, _ref1;
807
+ var _job_lease, _input_item, _this;
737
808
  const job = jobFrom(await tx.get(ref));
738
809
  if (!job) throw new Error(`ai job ${orgId}/${jobId} vanished`);
739
810
  const steps = job.steps.map((step, index)=>{
@@ -756,8 +827,11 @@ function nextStepIndex(job) {
756
827
  const items = ledger && input.item ? aiApplyJobItemRecord(ledger, input.item, input.creditsSpent, now) : input.items ? ledger : null;
757
828
  const settled = input.status !== 'pending';
758
829
  const ownsLease = ((_job_lease = job.lease) == null ? void 0 : _job_lease.owner) === owner;
759
- const parksForReview = Boolean(input.review) && !isAiJobTerminal(job.status);
760
- const status = isAiJobTerminal(job.status) ? job.status : job.status === 'needs_input' ? job.status : parksForReview ? 'needs_review' : input.status === 'failed' ? 'running' : remaining > 0 ? 'queued' : 'running';
830
+ // A cancel asked while this step ran ends the job here (AGL-3616), in the
831
+ // write that records what the step cost and built: nothing after it runs.
832
+ const canceling = aiJobCancelPending(job);
833
+ const parksForReview = Boolean(input.review) && !isAiJobTerminal(job.status) && !canceling;
834
+ const status = isAiJobTerminal(job.status) ? job.status : canceling ? 'canceled' : job.status === 'needs_input' ? job.status : parksForReview ? 'needs_review' : input.status === 'failed' ? 'running' : remaining > 0 ? 'queued' : 'running';
761
835
  const patch = _extends({
762
836
  status,
763
837
  steps,
@@ -783,7 +857,16 @@ function nextStepIndex(job) {
783
857
  } : {}, parksForReview && input.review ? {
784
858
  review: input.review,
785
859
  error: input.review.reason === 'plan' ? null : input.review.message
786
- } : {});
860
+ } : {}, canceling ? _extends({
861
+ creditsReserved: 0,
862
+ lease: null,
863
+ error: null,
864
+ steps: steps.map((step)=>step.status === 'running' ? _extends({}, step, {
865
+ status: 'pending'
866
+ }) : step)
867
+ }, ((_this = items != null ? items : job.items) == null ? void 0 : _this.length) ? {
868
+ items: aiCanceledItems((_ref1 = items != null ? items : job.items) != null ? _ref1 : [])
869
+ } : {}) : {});
787
870
  tx.set(ref, patch, {
788
871
  merge: true
789
872
  });
@@ -821,14 +904,14 @@ async function transition(firestore, orgId, jobId, apply) {
821
904
  });
822
905
  }
823
906
  /** Every step done: the job is `done` and holds nothing. */ export async function completeAiJob(firestore, orgId, jobId, now = new Date()) {
824
- const { job, changed } = await transition(firestore, orgId, jobId, (current)=>isAiJobTerminal(current.status) ? null : {
907
+ const { job, changed } = await transition(firestore, orgId, jobId, (current)=>isAiJobTerminal(current.status) ? null : aiJobCancelPending(current) ? aiJobCanceledPatch(current, now, cancelRequestOf(current, now)) : {
825
908
  status: 'done',
826
909
  creditsReserved: 0,
827
910
  lease: null,
828
911
  error: null,
829
912
  updatedAt: now
830
913
  });
831
- if (changed) await announceAiJobTransition(job, 'done');
914
+ if (changed && job.status === 'done') await announceAiJobTransition(job, 'done');
832
915
  return job;
833
916
  }
834
917
  /**
@@ -843,7 +926,7 @@ async function transition(firestore, orgId, jobId, apply) {
843
926
  stepIndex: (_ref = detail == null ? void 0 : detail.stepIndex) != null ? _ref : null,
844
927
  error: (detail == null ? void 0 : detail.error) instanceof Error ? detail.error.message : (_ref1 = detail == null ? void 0 : detail.error) != null ? _ref1 : null
845
928
  });
846
- const { job, changed } = await transition(firestore, orgId, jobId, (current)=>isAiJobTerminal(current.status) ? null : {
929
+ const { job, changed } = await transition(firestore, orgId, jobId, (current)=>isAiJobTerminal(current.status) ? null : aiJobCancelPending(current) ? aiJobCanceledPatch(current, now, cancelRequestOf(current, now)) : {
847
930
  status: 'failed',
848
931
  error: message,
849
932
  creditsReserved: 0,
@@ -855,7 +938,7 @@ async function transition(firestore, orgId, jobId, apply) {
855
938
  }) : step),
856
939
  updatedAt: now
857
940
  });
858
- if (changed) {
941
+ if (changed && job.status === 'failed') {
859
942
  var _ref2, _ref3;
860
943
  await announceAiJobTransition(job, 'failed', {
861
944
  ours: (_ref2 = detail == null ? void 0 : detail.ours) != null ? _ref2 : false,
@@ -870,7 +953,7 @@ async function transition(firestore, orgId, jobId, apply) {
870
953
  * step goes back to `pending` so the same step is what runs when the beat
871
954
  * tries again; its attempt is not counted, because nothing was attempted.
872
955
  */ export async function markAiJobNeedsInput(firestore, orgId, jobId, reason, now = new Date()) {
873
- const { job } = await transition(firestore, orgId, jobId, (current)=>isAiJobTerminal(current.status) ? null : {
956
+ const { job } = await transition(firestore, orgId, jobId, (current)=>isAiJobTerminal(current.status) ? null : aiJobCancelPending(current) ? aiJobCanceledPatch(current, now, cancelRequestOf(current, now)) : {
874
957
  status: 'needs_input',
875
958
  error: reason,
876
959
  lease: null,
@@ -886,23 +969,46 @@ async function transition(firestore, orgId, jobId, apply) {
886
969
  return job;
887
970
  }
888
971
  /**
889
- * Cancel. Idempotent — a terminal job is returned unchanged — and it does
890
- * not wait for a step in flight: the runner holding the lease finishes its
891
- * provider call, records what it cost, and finds the job canceled when it
892
- * goes to complete it.
972
+ * Cancel (AGL-2904, AGL-3616). One transaction decides it against whatever
973
+ * else commits, so a cancel racing a finish resolves to whichever committed
974
+ * first: a job already `done`, `failed` or `canceled` is returned unchanged.
893
975
  *
894
- * The activity row is written only when the cancel changed something: a
895
- * second cancel of the same job is not a second act. `actor` is the member
896
- * who canceled; a cancel with nobody named is recorded as nobody's rather
897
- * than as the creator's.
976
+ * - **No step in flight** (queued, parked for a person or the meter, or a
977
+ * lease that ran out): the job ends `canceled` in this write. It holds
978
+ * nothing, its unbuilt items are skipped, and nothing it built is touched.
979
+ * - **A step in flight** (a live lease): the cancel is recorded as
980
+ * `cancelRequested` and the job keeps its status until the runner holding
981
+ * the lease comes back. The runner watches for it and aborts its provider
982
+ * call; whatever the step had spent is metered, and the machine's next
983
+ * write ends the job `canceled` (`recordStep`, or the finish or failure it
984
+ * was about to write, which a pending cancel turns into the cancel).
985
+ *
986
+ * Either way a canceled job is never failed: no `ai.jobFailed` alert, no
987
+ * failure notice, no give-back (what ran stays charged), no publish.
988
+ *
989
+ * The activity row is written once, when the cancel is first asked: a second
990
+ * cancel of the same job is not a second act. `actor` is the member who
991
+ * canceled; a cancel with nobody named is recorded as nobody's rather than
992
+ * as the creator's.
898
993
  */ export async function cancelAiJob(firestore, orgId, jobId, now = new Date(), actor = null) {
899
- const result = await transition(firestore, orgId, jobId, (current)=>isAiJobTerminal(current.status) ? null : {
900
- status: 'canceled',
901
- creditsReserved: 0,
902
- lease: null,
994
+ var _ref;
995
+ const request = aiJobCancelRequest(now, (_ref = actor == null ? void 0 : actor.uid) != null ? _ref : null);
996
+ let firstAsk = false;
997
+ const result = await transition(firestore, orgId, jobId, (current)=>{
998
+ if (isAiJobTerminal(current.status)) return null;
999
+ const inFlight = anyLeaseIsLive(current, now.getTime());
1000
+ if (current.cancelRequested) {
1001
+ // Asked already: nothing new while its step is still in flight; a
1002
+ // step whose runner never came back is ended now.
1003
+ return inFlight ? null : aiJobCanceledPatch(current, now, current.cancelRequested);
1004
+ }
1005
+ firstAsk = true;
1006
+ return inFlight ? {
1007
+ cancelRequested: request,
903
1008
  updatedAt: now
904
- });
905
- if (result.changed) {
1009
+ } : aiJobCanceledPatch(current, now, request);
1010
+ });
1011
+ if (result.changed && firstAsk) {
906
1012
  await logAiJobCanceled(orgId, actor != null ? actor : {
907
1013
  uid: null
908
1014
  }, {
@@ -970,8 +1076,16 @@ async function transition(firestore, orgId, jobId, apply) {
970
1076
  * ended meanwhile — a person's cancel, which pays for what ran — is left as it
971
1077
  * ended and given nothing back.
972
1078
  */ async function failOurFailure(firestore, orgId, jobId, message, detail, refund, now) {
1079
+ var _detail_ours;
973
1080
  var _current_items;
974
1081
  const current = await getAiJob(firestore, orgId, jobId);
1082
+ // A person's cancel asked while the step ran is the job's end (AGL-3616):
1083
+ // not our failure, so nothing is given back and nobody is told.
1084
+ if (current && aiJobCancelPending(current)) {
1085
+ return failAiJob(firestore, orgId, jobId, message, _extends({}, detail, {
1086
+ ours: false
1087
+ }), now);
1088
+ }
975
1089
  // A build that got as far as its items settles item by item (AGL-3616):
976
1090
  // what it delivered stands, and what it did not is given back per item.
977
1091
  if (current && aiJobSettlesByItem(current.kind) && ((_current_items = current.items) == null ? void 0 : _current_items.length) && !isAiJobTerminal(current.status)) {
@@ -996,8 +1110,10 @@ async function transition(firestore, orgId, jobId, apply) {
996
1110
  now
997
1111
  }));
998
1112
  }
1113
+ // Given back as ours either way; ANNOUNCED as ours unless the step said the
1114
+ // failure is the customer's own limit (`customerLimit`), which pages no one.
999
1115
  return failAiJob(firestore, orgId, jobId, message, _extends({}, detail, {
1000
- ours: true
1116
+ ours: (_detail_ours = detail.ours) != null ? _detail_ours : true
1001
1117
  }), now);
1002
1118
  }
1003
1119
  // ── A build's settlement (AGL-3616) ──────────────────────────────────────
@@ -1085,6 +1201,8 @@ async function transition(firestore, orgId, jobId, apply) {
1085
1201
  const { now } = input;
1086
1202
  let job = await getAiJob(firestore, orgId, jobId);
1087
1203
  if (!job || isAiJobTerminal(job.status)) return job;
1204
+ // A cancel asked before the build settled is how it ends (AGL-3616).
1205
+ if (aiJobCancelPending(job)) return completeAiJob(firestore, orgId, jobId, now);
1088
1206
  const stopped = input.stopped;
1089
1207
  if (stopped && ((_job_items = job.items) != null ? _job_items : []).some((row)=>row.status === 'pending' || row.status === 'running')) {
1090
1208
  var _job_items4;
@@ -1222,7 +1340,7 @@ async function transition(firestore, orgId, jobId, apply) {
1222
1340
  * re-asks the plan. Held at what the retried items are estimated to cost.
1223
1341
  * `changed: false` for a job that is not a finished build, or has nothing to
1224
1342
  * try again.
1225
- */ export async function retryAiBuildJob(firestore, orgId, jobId, now = new Date()) {
1343
+ */ export async function retryAiBuildJob(firestore, orgId, jobId, now = new Date(), options = {}) {
1226
1344
  let retried = [];
1227
1345
  const result = await transition(firestore, orgId, jobId, (current)=>{
1228
1346
  var _current_plan, _current_items;
@@ -1232,7 +1350,7 @@ async function transition(firestore, orgId, jobId, apply) {
1232
1350
  const next = aiBuildRetryLedger(current.items, current.kind === 'site' ? aiLedgerUnits(current.items) : aiBuildUnits(plan));
1233
1351
  if (!next.ledger.some((row)=>row.status === 'pending')) return null;
1234
1352
  retried = next.retried;
1235
- return {
1353
+ return _extends({
1236
1354
  status: 'queued',
1237
1355
  items: next.ledger,
1238
1356
  steps: current.steps.map((step)=>step.name === AI_JOB_PLAN_STEP ? step : _extends({}, step, {
@@ -1247,9 +1365,12 @@ async function transition(firestore, orgId, jobId, apply) {
1247
1365
  lease: null,
1248
1366
  creditsReserved: Math.max(AI_JOB_STEP_RESERVE_CREDITS, current.kind === 'site' ? next.retried.length * AI_JOB_STEP_RESERVE_CREDITS : aiBuildCreditEstimate(plan, {
1249
1367
  slots: new Set(next.retried)
1250
- })),
1368
+ }))
1369
+ }, options.creditsConfirmed ? {
1370
+ creditsConfirmed: options.creditsConfirmed
1371
+ } : {}, {
1251
1372
  updatedAt: now
1252
- };
1373
+ });
1253
1374
  });
1254
1375
  return _extends({}, result, {
1255
1376
  retried: result.changed ? retried : []
@@ -1268,6 +1389,7 @@ async function transition(firestore, orgId, jobId, apply) {
1268
1389
  * (AGL-3031), creations included, where that is more.
1269
1390
  */ export async function resumeAiJob(firestore, orgId, jobId, actor, now = new Date(), options = {}) {
1270
1391
  return transition(firestore, orgId, jobId, (current)=>{
1392
+ var _options_reducePlan;
1271
1393
  var _current_review, _current_inputs;
1272
1394
  // A job the meter paused (AGL-3660) carries on from the step it paused
1273
1395
  // on: that step is already pending, so it is queued again with nothing
@@ -1287,7 +1409,8 @@ async function transition(firestore, orgId, jobId, apply) {
1287
1409
  }) : step);
1288
1410
  const outstanding = steps.filter((step)=>step.status === 'pending').length;
1289
1411
  const pending = outstanding > 0;
1290
- const confirming = ((_current_review = current.review) == null ? void 0 : _current_review.reason) === 'plan' && current.plan ? current.plan : null;
1412
+ const proposed = ((_current_review = current.review) == null ? void 0 : _current_review.reason) === 'plan' && current.plan ? current.plan : null;
1413
+ const confirming = proposed && options.reducePlan ? (_options_reducePlan = options.reducePlan(proposed)) != null ? _options_reducePlan : proposed : proposed;
1291
1414
  const confirmed = confirming ? {
1292
1415
  plan: _extends({}, confirming, {
1293
1416
  status: 'confirmed',
@@ -1305,7 +1428,9 @@ async function transition(firestore, orgId, jobId, apply) {
1305
1428
  steps,
1306
1429
  review: null,
1307
1430
  error: null
1308
- }, confirmed, publish, {
1431
+ }, confirmed, publish, options.creditsConfirmed ? {
1432
+ creditsConfirmed: options.creditsConfirmed
1433
+ } : {}, {
1309
1434
  creditsReserved: pending ? Math.max(outstanding * AI_JOB_STEP_RESERVE_CREDITS, confirming ? aiJobCreditEstimate(current.kind, confirming) : 0) : 0,
1310
1435
  updatedAt: now
1311
1436
  });
@@ -1410,7 +1535,7 @@ async function transition(firestore, orgId, jobId, apply) {
1410
1535
  * round.
1411
1536
  */ export async function runAiJobStep(firestore, orgId, jobId, options) {
1412
1537
  var _options_now, _step_attempts, _options_org, _data, _ref, _ref1, _outcome_effort, _step_passes, _options_actor;
1413
- var _reservation_allotment, _reservation_allotment1, _outcome_review, _itemRecord_failure, _recorded_job_items;
1538
+ var _reservation_allotment, _reservation_allotment1, _options_signal, _options_signal1, _outcome_review, _itemRecord_failure, _recorded_job_items;
1414
1539
  const now = (_options_now = options.now) != null ? _options_now : new Date();
1415
1540
  // A reservation the caller took before the job existed is a message
1416
1541
  // counted against the org; a step that fails before the provider is
@@ -1527,6 +1652,11 @@ async function transition(firestore, orgId, jobId, apply) {
1527
1652
  if (!reservation.allowed) {
1528
1653
  const refusal = aiJobRefusalText(org, reservation);
1529
1654
  const parked = await markAiJobNeedsInput(firestore, orgId, jobId, refusal, now);
1655
+ // A cancel asked meanwhile ended the job instead of parking it (AGL-3616).
1656
+ if (parked.status === 'canceled') return {
1657
+ outcome: 'canceled',
1658
+ job: parked
1659
+ };
1530
1660
  // Nobody parked the job — the meter did — so the row carries no actor
1531
1661
  // rather than the creator's name on an act they did not perform. Only
1532
1662
  // when the job was not already parked for this: the reason the last
@@ -1565,6 +1695,21 @@ async function transition(firestore, orgId, jobId, apply) {
1565
1695
  var _resolveAiModelChoice;
1566
1696
  return (_resolveAiModelChoice = resolveAiModelChoice(kind, (_job_model = job.model) != null ? _job_model : null, bounds)) == null ? void 0 : _resolveAiModelChoice.model;
1567
1697
  };
1698
+ // A person's cancel stops the step (AGL-3616): the step's signal is the
1699
+ // caller's budget AND a watch on the job, so a cancel aborts the provider
1700
+ // call in flight, stops one about to start, and stops a write the step
1701
+ // asks `throwIfAiJobCanceled` about first.
1702
+ const stepSignal = new AbortController();
1703
+ const forwardBudget = ()=>{
1704
+ var _options_signal;
1705
+ return stepSignal.abort((_options_signal = options.signal) == null ? void 0 : _options_signal.reason);
1706
+ };
1707
+ if ((_options_signal = options.signal) == null ? void 0 : _options_signal.aborted) forwardBudget();
1708
+ else (_options_signal1 = options.signal) == null ? void 0 : _options_signal1.addEventListener('abort', forwardBudget, {
1709
+ once: true
1710
+ });
1711
+ const stopWatch = watchAiJobCancel(()=>getAiJob(firestore, orgId, jobId), stepSignal, options.cancelPollMs);
1712
+ const canceledMidStep = ()=>stepSignal.signal.aborted && isAiJobCanceledError(stepSignal.signal.reason);
1568
1713
  let outcome;
1569
1714
  // The runner's wall-clock time, recorded on the step beside its tokens
1570
1715
  // (AGL-2937): measured here, around the call, rather than asked of it.
@@ -1574,7 +1719,7 @@ async function transition(firestore, orgId, jobId, apply) {
1574
1719
  job,
1575
1720
  stepIndex,
1576
1721
  now,
1577
- signal: options.signal,
1722
+ signal: stepSignal.signal,
1578
1723
  firestore,
1579
1724
  org,
1580
1725
  modelFor
@@ -1590,6 +1735,18 @@ async function transition(firestore, orgId, jobId, apply) {
1590
1735
  jobId,
1591
1736
  releaseError
1592
1737
  }));
1738
+ // Canceled mid-step (AGL-3616): the step is handed back unrun and the
1739
+ // record ends the job canceled — never failed, never retried.
1740
+ if (isAiJobCanceledError(error) || canceledMidStep()) {
1741
+ const { job: canceled } = await recordStep(firestore, orgId, jobId, options.owner, stepIndex, {
1742
+ status: 'pending',
1743
+ creditsSpent: 0
1744
+ }, now);
1745
+ return {
1746
+ outcome: 'canceled',
1747
+ job: canceled
1748
+ };
1749
+ }
1593
1750
  const retryable = error instanceof AiUpstreamError && error.retryable || isAbort(error);
1594
1751
  if (retryable) {
1595
1752
  const { job: requeued } = await recordStep(firestore, orgId, jobId, options.owner, stepIndex, {
@@ -1612,6 +1769,10 @@ async function transition(firestore, orgId, jobId, apply) {
1612
1769
  free: (_reservation_free = reservation.free) != null ? _reservation_free : null
1613
1770
  }, now)
1614
1771
  };
1772
+ } finally{
1773
+ var _options_signal2;
1774
+ stopWatch();
1775
+ (_options_signal2 = options.signal) == null ? void 0 : _options_signal2.removeEventListener('abort', forwardBudget);
1615
1776
  }
1616
1777
  const latencyMs = Math.max(0, Date.now() - runStarted);
1617
1778
  // A step that failed before it reached the provider — the site it builds
@@ -1632,10 +1793,12 @@ async function transition(firestore, orgId, jobId, apply) {
1632
1793
  }, now);
1633
1794
  return {
1634
1795
  outcome: 'failed',
1635
- job: await failOurFailure(firestore, orgId, jobId, outcome.failure, {
1796
+ job: await failOurFailure(firestore, orgId, jobId, outcome.failure, _extends({
1636
1797
  stepIndex,
1637
1798
  error: `step failure before the provider: ${outcome.failure}`
1638
- }, {
1799
+ }, outcome.customerLimit ? {
1800
+ ours: false
1801
+ } : {}), {
1639
1802
  reason: 'step-failure',
1640
1803
  stepCredits: 0,
1641
1804
  free: (_reservation_free1 = reservation.free) != null ? _reservation_free1 : null
@@ -1703,10 +1866,12 @@ async function transition(firestore, orgId, jobId, apply) {
1703
1866
  error: message,
1704
1867
  tokens
1705
1868
  }, now);
1706
- const detail = {
1869
+ const detail = _extends({
1707
1870
  stepIndex,
1708
1871
  error: outcome.refused ? 'stop_reason refusal' : `step failure: ${message}`
1709
- };
1872
+ }, outcome.customerLimit ? {
1873
+ ours: false
1874
+ } : {});
1710
1875
  // A model declining the brief is not our failure; a step's own is (AGL-3594).
1711
1876
  return {
1712
1877
  outcome: 'failed',
@@ -1758,7 +1923,9 @@ async function transition(firestore, orgId, jobId, apply) {
1758
1923
  // A build item that failed on our side gives back its own attempt's spend
1759
1924
  // before its row is written (AGL-3616), so the row records what was given.
1760
1925
  let itemRecord = aiJobSettlesByItem(job.kind) ? outcome.item : undefined;
1761
- if ((itemRecord == null ? void 0 : itemRecord.status) === 'failed' && ((_itemRecord_failure = itemRecord.failure) == null ? void 0 : _itemRecord_failure.ours)) {
1926
+ // An item a person's cancel cut short is not our failure (AGL-3616): it is
1927
+ // given nothing back, and its row is the cancel's.
1928
+ if ((itemRecord == null ? void 0 : itemRecord.status) === 'failed' && ((_itemRecord_failure = itemRecord.failure) == null ? void 0 : _itemRecord_failure.ours) && !canceledMidStep()) {
1762
1929
  var _ref2, _outcome_items, _ref3, _ref4, _reservation_free5;
1763
1930
  const slot = itemRecord.slot;
1764
1931
  const row = ((_ref2 = (_outcome_items = outcome.items) != null ? _outcome_items : job.items) != null ? _ref2 : []).find((one)=>one.slot === slot);
@@ -1839,6 +2006,11 @@ async function transition(firestore, orgId, jobId, apply) {
1839
2006
  }
1840
2007
  });
1841
2008
  }
2009
+ // A cancel asked while the step ran ended the job in that record (AGL-3616).
2010
+ if (recorded.job.status === 'canceled') return {
2011
+ outcome: 'canceled',
2012
+ job: recorded.job
2013
+ };
1842
2014
  if ((review == null ? void 0 : review.reason) === 'plan' && recorded.job.status === 'needs_review' && aiJobAutoConfirms(job)) {
1843
2015
  // A guided site start confirms its own plan (AGL-3594): the resume
1844
2016
  // door's confirmation, by the job's creator, and the build queued.