@aglyn/plugins-ai 1.0.0-beta.237 → 1.0.0-beta.239

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 (172) hide show
  1. package/package.json +17 -17
  2. package/src/lib/components/ai-credits-prompt.component.d.ts +11 -4
  3. package/src/lib/components/ai-credits-prompt.component.js +84 -34
  4. package/src/lib/components/ai-credits-prompt.component.js.map +1 -1
  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-links.js +8 -0
  9. package/src/lib/components/ai-job-links.js.map +1 -1
  10. package/src/lib/components/ai-job-plan.component.js +12 -1
  11. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  12. package/src/lib/components/ai-jobs-list-page.component.js +141 -72
  13. package/src/lib/components/ai-jobs-list-page.component.js.map +1 -1
  14. package/src/lib/components/ai-jobs-store.d.ts +12 -0
  15. package/src/lib/components/ai-jobs-store.js +15 -0
  16. package/src/lib/components/ai-jobs-store.js.map +1 -1
  17. package/src/lib/components/ai-site-build-page.component.js +22 -2
  18. package/src/lib/components/ai-site-build-page.component.js.map +1 -1
  19. package/src/lib/components/ai-usage-meter-refresh.d.ts +35 -0
  20. package/src/lib/components/ai-usage-meter-refresh.js +63 -0
  21. package/src/lib/components/ai-usage-meter-refresh.js.map +1 -0
  22. package/src/lib/components/ai-usage-strip.component.d.ts +19 -2
  23. package/src/lib/components/ai-usage-strip.component.js +40 -9
  24. package/src/lib/components/ai-usage-strip.component.js.map +1 -1
  25. package/src/lib/components/assist-jobs-drawer.component.js +12 -16
  26. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  27. package/src/lib/components/assist-panel.component.js +91 -51
  28. package/src/lib/components/assist-panel.component.js.map +1 -1
  29. package/src/lib/components/use-ai-usage-meter.d.ts +2 -0
  30. package/src/lib/components/use-ai-usage-meter.js +8 -3
  31. package/src/lib/components/use-ai-usage-meter.js.map +1 -1
  32. package/src/lib/jobs/ai-build-unit-outcome.d.ts +3 -1
  33. package/src/lib/jobs/ai-build-unit-outcome.js +19 -3
  34. package/src/lib/jobs/ai-build-unit-outcome.js.map +1 -1
  35. package/src/lib/jobs/ai-job-build-step.js +33 -3
  36. package/src/lib/jobs/ai-job-build-step.js.map +1 -1
  37. package/src/lib/jobs/ai-job-cancel.d.ts +71 -0
  38. package/src/lib/jobs/ai-job-cancel.js +102 -0
  39. package/src/lib/jobs/ai-job-cancel.js.map +1 -0
  40. package/src/lib/jobs/ai-job-component-step.js +4 -1
  41. package/src/lib/jobs/ai-job-component-step.js.map +1 -1
  42. package/src/lib/jobs/ai-job-draft-ids.js +7 -2
  43. package/src/lib/jobs/ai-job-draft-ids.js.map +1 -1
  44. package/src/lib/jobs/ai-job-drafts.js +2 -0
  45. package/src/lib/jobs/ai-job-drafts.js.map +1 -1
  46. package/src/lib/jobs/ai-job-form-dataset.d.ts +100 -0
  47. package/src/lib/jobs/ai-job-form-dataset.js +161 -0
  48. package/src/lib/jobs/ai-job-form-dataset.js.map +1 -0
  49. package/src/lib/jobs/ai-job-form-step.d.ts +15 -3
  50. package/src/lib/jobs/ai-job-form-step.js +41 -10
  51. package/src/lib/jobs/ai-job-form-step.js.map +1 -1
  52. package/src/lib/jobs/ai-job-layout-language.d.ts +2 -0
  53. package/src/lib/jobs/ai-job-layout-language.js +13 -1
  54. package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
  55. package/src/lib/jobs/ai-job-layout-step.js +6 -1
  56. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  57. package/src/lib/jobs/ai-job-page-language.d.ts +9 -0
  58. package/src/lib/jobs/ai-job-page-language.js +45 -2
  59. package/src/lib/jobs/ai-job-page-language.js.map +1 -1
  60. package/src/lib/jobs/ai-job-page-step.js +12 -1
  61. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  62. package/src/lib/jobs/ai-job-plan-step.d.ts +38 -1
  63. package/src/lib/jobs/ai-job-plan-step.js +90 -13
  64. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  65. package/src/lib/jobs/ai-job-site-content.d.ts +15 -0
  66. package/src/lib/jobs/ai-job-site-content.js +66 -7
  67. package/src/lib/jobs/ai-job-site-content.js.map +1 -1
  68. package/src/lib/jobs/ai-job-site-datasets.d.ts +170 -0
  69. package/src/lib/jobs/ai-job-site-datasets.js +499 -0
  70. package/src/lib/jobs/ai-job-site-datasets.js.map +1 -0
  71. package/src/lib/jobs/ai-job-site-step.d.ts +15 -8
  72. package/src/lib/jobs/ai-job-site-step.js +152 -30
  73. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  74. package/src/lib/jobs/ai-job-template-step.js +4 -1
  75. package/src/lib/jobs/ai-job-template-step.js.map +1 -1
  76. package/src/lib/jobs/ai-jobs.d.ts +37 -12
  77. package/src/lib/jobs/ai-jobs.js +184 -28
  78. package/src/lib/jobs/ai-jobs.js.map +1 -1
  79. package/src/lib/jobs/ai-layout-stock-photos.d.ts +143 -70
  80. package/src/lib/jobs/ai-layout-stock-photos.js +649 -176
  81. package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
  82. package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.d.ts +30 -0
  83. package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.js +220 -0
  84. package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.js.map +1 -0
  85. package/src/lib/jobs/fixtures/ai-dataset-site-plan-recordings.d.ts +30 -0
  86. package/src/lib/jobs/fixtures/ai-dataset-site-plan-recordings.js +637 -0
  87. package/src/lib/jobs/fixtures/ai-dataset-site-plan-recordings.js.map +1 -0
  88. package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.d.ts +27 -0
  89. package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.js +151 -0
  90. package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.js.map +1 -0
  91. package/src/lib/layout-language/ai-layout-compiler.d.ts +16 -0
  92. package/src/lib/layout-language/ai-layout-compiler.js +397 -9
  93. package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
  94. package/src/lib/layout-language/ai-layout-design.d.ts +6 -3
  95. package/src/lib/layout-language/ai-layout-design.js +5 -3
  96. package/src/lib/layout-language/ai-layout-design.js.map +1 -1
  97. package/src/lib/layout-language/ai-layout-frame.d.ts +12 -0
  98. package/src/lib/layout-language/ai-layout-frame.js +58 -3
  99. package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
  100. package/src/lib/layout-language/ai-layout-gaps.js +17 -1
  101. package/src/lib/layout-language/ai-layout-gaps.js.map +1 -1
  102. package/src/lib/layout-language/ai-layout-listings.d.ts +44 -3
  103. package/src/lib/layout-language/ai-layout-listings.js +118 -50
  104. package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
  105. package/src/lib/layout-language/ai-layout-pictures.d.ts +8 -0
  106. package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
  107. package/src/lib/layout-language/ai-layout-storefront.d.ts +82 -0
  108. package/src/lib/layout-language/ai-layout-storefront.js +223 -0
  109. package/src/lib/layout-language/ai-layout-storefront.js.map +1 -0
  110. package/src/lib/model/ai-build-job.d.ts +11 -1
  111. package/src/lib/model/ai-build-job.js +35 -6
  112. package/src/lib/model/ai-build-job.js.map +1 -1
  113. package/src/lib/model/ai-build-plan.d.ts +23 -3
  114. package/src/lib/model/ai-build-plan.js +47 -6
  115. package/src/lib/model/ai-build-plan.js.map +1 -1
  116. package/src/lib/model/ai-build-progress.d.ts +2 -2
  117. package/src/lib/model/ai-build-progress.js +6 -4
  118. package/src/lib/model/ai-build-progress.js.map +1 -1
  119. package/src/lib/model/ai-credit-estimate.d.ts +69 -2
  120. package/src/lib/model/ai-credit-estimate.js +91 -3
  121. package/src/lib/model/ai-credit-estimate.js.map +1 -1
  122. package/src/lib/model/ai-job-cancel-copy.d.ts +44 -0
  123. package/src/lib/model/ai-job-cancel-copy.js +53 -0
  124. package/src/lib/model/ai-job-cancel-copy.js.map +1 -0
  125. package/src/lib/model/ai-job-failure-copy.d.ts +4 -1
  126. package/src/lib/model/ai-job-failure-copy.js +8 -3
  127. package/src/lib/model/ai-job-failure-copy.js.map +1 -1
  128. package/src/lib/model/ai-jobs.types.d.ts +20 -0
  129. package/src/lib/model/ai-jobs.types.js.map +1 -1
  130. package/src/lib/model/ai-plan-capabilities.d.ts +7 -0
  131. package/src/lib/model/ai-plan-capabilities.js.map +1 -1
  132. package/src/lib/model/ai-site-build-progress.js +7 -4
  133. package/src/lib/model/ai-site-build-progress.js.map +1 -1
  134. package/src/lib/model/ai-site-job.d.ts +38 -4
  135. package/src/lib/model/ai-site-job.js +50 -14
  136. package/src/lib/model/ai-site-job.js.map +1 -1
  137. package/src/lib/model/ai-site-kinds.d.ts +4 -1
  138. package/src/lib/model/ai-site-kinds.js +34 -6
  139. package/src/lib/model/ai-site-kinds.js.map +1 -1
  140. package/src/lib/runtime/ai-dataset-generation.d.ts +77 -0
  141. package/src/lib/runtime/ai-dataset-generation.js +127 -0
  142. package/src/lib/runtime/ai-dataset-generation.js.map +1 -0
  143. package/src/lib/runtime/ai-doctrine-validators.d.ts +77 -1
  144. package/src/lib/runtime/ai-doctrine-validators.js +280 -6
  145. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  146. package/src/lib/runtime/ai-doctrine.js +10 -4
  147. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  148. package/src/lib/runtime/ai-node-tree.d.ts +2 -0
  149. package/src/lib/runtime/ai-node-tree.js +16 -0
  150. package/src/lib/runtime/ai-node-tree.js.map +1 -1
  151. package/src/lib/runtime/ai-palette.generated.js +53 -7
  152. package/src/lib/runtime/ai-palette.generated.js.map +1 -1
  153. package/src/lib/server/ai-jobs-cancel.d.ts +13 -5
  154. package/src/lib/server/ai-jobs-cancel.js +27 -9
  155. package/src/lib/server/ai-jobs-cancel.js.map +1 -1
  156. package/src/lib/server/ai-jobs-gate.d.ts +12 -2
  157. package/src/lib/server/ai-jobs-gate.js +20 -9
  158. package/src/lib/server/ai-jobs-gate.js.map +1 -1
  159. package/src/lib/tools/ai-dataset-tool.d.ts +68 -0
  160. package/src/lib/tools/ai-dataset-tool.js +231 -0
  161. package/src/lib/tools/ai-dataset-tool.js.map +1 -0
  162. package/src/lib/usage/ai-usage-meter.d.ts +2 -1
  163. package/src/lib/usage/ai-usage-meter.js +17 -1
  164. package/src/lib/usage/ai-usage-meter.js.map +1 -1
  165. package/src/lib/usage/ai-usage-wire.d.ts +6 -0
  166. package/src/lib/usage/ai-usage-wire.js.map +1 -1
  167. package/src/lib/usage/assist-usage.d.ts +7 -0
  168. package/src/lib/usage/assist-usage.js +14 -4
  169. package/src/lib/usage/assist-usage.js.map +1 -1
  170. package/src/lib/usage/free-ai-credits-left.d.ts +7 -0
  171. package/src/lib/usage/free-ai-credits-left.js +30 -4
  172. 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
  *
@@ -524,6 +525,8 @@ export async function getAiJob(firestore, orgId, jobId) {
524
525
  orchestration: job.orchestration
525
526
  } : {}, job.kind === 'build' && ((_job_inputs1 = job.inputs) == null ? void 0 : _job_inputs1['publish']) === true ? {
526
527
  publishAsked: true
528
+ } : {}, aiJobCancelPending(job) ? {
529
+ cancelRequested: true
527
530
  } : {});
528
531
  }
529
532
  function leaseIsLive(job, owner, nowMs) {
@@ -533,6 +536,52 @@ function leaseIsLive(job, owner, nowMs) {
533
536
  const until = toMillis(lease.until);
534
537
  return until !== null && until > nowMs;
535
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
+ }
536
585
  function nextStepIndex(job) {
537
586
  var _job_steps;
538
587
  return ((_job_steps = job.steps) != null ? _job_steps : []).findIndex((step)=>step.status === 'pending' || step.status === 'running');
@@ -561,6 +610,16 @@ function nextStepIndex(job) {
561
610
  return firestore.runTransaction(async (tx)=>{
562
611
  const job = jobFrom(await tx.get(ref));
563
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
+ }
564
623
  const stepIndex = claimableStepIndex(job, owner, now.getTime());
565
624
  if (stepIndex === -1) return null;
566
625
  const steps = job.steps.map((step, index)=>{
@@ -632,7 +691,17 @@ function nextStepIndex(job) {
632
691
  const ref = jobsCollection(firestore, orgId).doc(jobId);
633
692
  return firestore.runTransaction(async (tx)=>{
634
693
  const job = jobFrom(await tx.get(ref));
635
- 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;
636
705
  tx.set(ref, {
637
706
  updatedAt: now
638
707
  }, {
@@ -734,8 +803,8 @@ function nextStepIndex(job) {
734
803
  */ export async function recordStep(firestore, orgId, jobId, owner, stepIndex, input, now = new Date()) {
735
804
  const ref = jobsCollection(firestore, orgId).doc(jobId);
736
805
  const recorded = await firestore.runTransaction(async (tx)=>{
737
- var _ref, _input_items, _job_outputs, _input_outputs, _job_creditsSpent, _job_creditsReserved, _job_creditsReserved1, _job_refundedCredits;
738
- 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;
739
808
  const job = jobFrom(await tx.get(ref));
740
809
  if (!job) throw new Error(`ai job ${orgId}/${jobId} vanished`);
741
810
  const steps = job.steps.map((step, index)=>{
@@ -758,8 +827,11 @@ function nextStepIndex(job) {
758
827
  const items = ledger && input.item ? aiApplyJobItemRecord(ledger, input.item, input.creditsSpent, now) : input.items ? ledger : null;
759
828
  const settled = input.status !== 'pending';
760
829
  const ownsLease = ((_job_lease = job.lease) == null ? void 0 : _job_lease.owner) === owner;
761
- const parksForReview = Boolean(input.review) && !isAiJobTerminal(job.status);
762
- 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';
763
835
  const patch = _extends({
764
836
  status,
765
837
  steps,
@@ -785,7 +857,16 @@ function nextStepIndex(job) {
785
857
  } : {}, parksForReview && input.review ? {
786
858
  review: input.review,
787
859
  error: input.review.reason === 'plan' ? null : input.review.message
788
- } : {});
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
+ } : {}) : {});
789
870
  tx.set(ref, patch, {
790
871
  merge: true
791
872
  });
@@ -823,14 +904,14 @@ async function transition(firestore, orgId, jobId, apply) {
823
904
  });
824
905
  }
825
906
  /** Every step done: the job is `done` and holds nothing. */ export async function completeAiJob(firestore, orgId, jobId, now = new Date()) {
826
- 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)) : {
827
908
  status: 'done',
828
909
  creditsReserved: 0,
829
910
  lease: null,
830
911
  error: null,
831
912
  updatedAt: now
832
913
  });
833
- if (changed) await announceAiJobTransition(job, 'done');
914
+ if (changed && job.status === 'done') await announceAiJobTransition(job, 'done');
834
915
  return job;
835
916
  }
836
917
  /**
@@ -845,7 +926,7 @@ async function transition(firestore, orgId, jobId, apply) {
845
926
  stepIndex: (_ref = detail == null ? void 0 : detail.stepIndex) != null ? _ref : null,
846
927
  error: (detail == null ? void 0 : detail.error) instanceof Error ? detail.error.message : (_ref1 = detail == null ? void 0 : detail.error) != null ? _ref1 : null
847
928
  });
848
- 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)) : {
849
930
  status: 'failed',
850
931
  error: message,
851
932
  creditsReserved: 0,
@@ -857,7 +938,7 @@ async function transition(firestore, orgId, jobId, apply) {
857
938
  }) : step),
858
939
  updatedAt: now
859
940
  });
860
- if (changed) {
941
+ if (changed && job.status === 'failed') {
861
942
  var _ref2, _ref3;
862
943
  await announceAiJobTransition(job, 'failed', {
863
944
  ours: (_ref2 = detail == null ? void 0 : detail.ours) != null ? _ref2 : false,
@@ -872,7 +953,7 @@ async function transition(firestore, orgId, jobId, apply) {
872
953
  * step goes back to `pending` so the same step is what runs when the beat
873
954
  * tries again; its attempt is not counted, because nothing was attempted.
874
955
  */ export async function markAiJobNeedsInput(firestore, orgId, jobId, reason, now = new Date()) {
875
- 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)) : {
876
957
  status: 'needs_input',
877
958
  error: reason,
878
959
  lease: null,
@@ -888,23 +969,46 @@ async function transition(firestore, orgId, jobId, apply) {
888
969
  return job;
889
970
  }
890
971
  /**
891
- * Cancel. Idempotent — a terminal job is returned unchanged — and it does
892
- * not wait for a step in flight: the runner holding the lease finishes its
893
- * provider call, records what it cost, and finds the job canceled when it
894
- * 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.
895
975
  *
896
- * The activity row is written only when the cancel changed something: a
897
- * second cancel of the same job is not a second act. `actor` is the member
898
- * who canceled; a cancel with nobody named is recorded as nobody's rather
899
- * 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.
900
993
  */ export async function cancelAiJob(firestore, orgId, jobId, now = new Date(), actor = null) {
901
- const result = await transition(firestore, orgId, jobId, (current)=>isAiJobTerminal(current.status) ? null : {
902
- status: 'canceled',
903
- creditsReserved: 0,
904
- 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,
905
1008
  updatedAt: now
906
- });
907
- if (result.changed) {
1009
+ } : aiJobCanceledPatch(current, now, request);
1010
+ });
1011
+ if (result.changed && firstAsk) {
908
1012
  await logAiJobCanceled(orgId, actor != null ? actor : {
909
1013
  uid: null
910
1014
  }, {
@@ -975,6 +1079,13 @@ async function transition(firestore, orgId, jobId, apply) {
975
1079
  var _detail_ours;
976
1080
  var _current_items;
977
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
+ }
978
1089
  // A build that got as far as its items settles item by item (AGL-3616):
979
1090
  // what it delivered stands, and what it did not is given back per item.
980
1091
  if (current && aiJobSettlesByItem(current.kind) && ((_current_items = current.items) == null ? void 0 : _current_items.length) && !isAiJobTerminal(current.status)) {
@@ -1090,6 +1201,8 @@ async function transition(firestore, orgId, jobId, apply) {
1090
1201
  const { now } = input;
1091
1202
  let job = await getAiJob(firestore, orgId, jobId);
1092
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);
1093
1206
  const stopped = input.stopped;
1094
1207
  if (stopped && ((_job_items = job.items) != null ? _job_items : []).some((row)=>row.status === 'pending' || row.status === 'running')) {
1095
1208
  var _job_items4;
@@ -1422,7 +1535,7 @@ async function transition(firestore, orgId, jobId, apply) {
1422
1535
  * round.
1423
1536
  */ export async function runAiJobStep(firestore, orgId, jobId, options) {
1424
1537
  var _options_now, _step_attempts, _options_org, _data, _ref, _ref1, _outcome_effort, _step_passes, _options_actor;
1425
- 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;
1426
1539
  const now = (_options_now = options.now) != null ? _options_now : new Date();
1427
1540
  // A reservation the caller took before the job existed is a message
1428
1541
  // counted against the org; a step that fails before the provider is
@@ -1539,6 +1652,11 @@ async function transition(firestore, orgId, jobId, apply) {
1539
1652
  if (!reservation.allowed) {
1540
1653
  const refusal = aiJobRefusalText(org, reservation);
1541
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
+ };
1542
1660
  // Nobody parked the job — the meter did — so the row carries no actor
1543
1661
  // rather than the creator's name on an act they did not perform. Only
1544
1662
  // when the job was not already parked for this: the reason the last
@@ -1577,6 +1695,21 @@ async function transition(firestore, orgId, jobId, apply) {
1577
1695
  var _resolveAiModelChoice;
1578
1696
  return (_resolveAiModelChoice = resolveAiModelChoice(kind, (_job_model = job.model) != null ? _job_model : null, bounds)) == null ? void 0 : _resolveAiModelChoice.model;
1579
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);
1580
1713
  let outcome;
1581
1714
  // The runner's wall-clock time, recorded on the step beside its tokens
1582
1715
  // (AGL-2937): measured here, around the call, rather than asked of it.
@@ -1586,7 +1719,7 @@ async function transition(firestore, orgId, jobId, apply) {
1586
1719
  job,
1587
1720
  stepIndex,
1588
1721
  now,
1589
- signal: options.signal,
1722
+ signal: stepSignal.signal,
1590
1723
  firestore,
1591
1724
  org,
1592
1725
  modelFor
@@ -1602,6 +1735,18 @@ async function transition(firestore, orgId, jobId, apply) {
1602
1735
  jobId,
1603
1736
  releaseError
1604
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
+ }
1605
1750
  const retryable = error instanceof AiUpstreamError && error.retryable || isAbort(error);
1606
1751
  if (retryable) {
1607
1752
  const { job: requeued } = await recordStep(firestore, orgId, jobId, options.owner, stepIndex, {
@@ -1624,6 +1769,10 @@ async function transition(firestore, orgId, jobId, apply) {
1624
1769
  free: (_reservation_free = reservation.free) != null ? _reservation_free : null
1625
1770
  }, now)
1626
1771
  };
1772
+ } finally{
1773
+ var _options_signal2;
1774
+ stopWatch();
1775
+ (_options_signal2 = options.signal) == null ? void 0 : _options_signal2.removeEventListener('abort', forwardBudget);
1627
1776
  }
1628
1777
  const latencyMs = Math.max(0, Date.now() - runStarted);
1629
1778
  // A step that failed before it reached the provider — the site it builds
@@ -1774,7 +1923,9 @@ async function transition(firestore, orgId, jobId, apply) {
1774
1923
  // A build item that failed on our side gives back its own attempt's spend
1775
1924
  // before its row is written (AGL-3616), so the row records what was given.
1776
1925
  let itemRecord = aiJobSettlesByItem(job.kind) ? outcome.item : undefined;
1777
- 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()) {
1778
1929
  var _ref2, _outcome_items, _ref3, _ref4, _reservation_free5;
1779
1930
  const slot = itemRecord.slot;
1780
1931
  const row = ((_ref2 = (_outcome_items = outcome.items) != null ? _outcome_items : job.items) != null ? _ref2 : []).find((one)=>one.slot === slot);
@@ -1855,6 +2006,11 @@ async function transition(firestore, orgId, jobId, apply) {
1855
2006
  }
1856
2007
  });
1857
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
+ };
1858
2014
  if ((review == null ? void 0 : review.reason) === 'plan' && recorded.job.status === 'needs_review' && aiJobAutoConfirms(job)) {
1859
2015
  // A guided site start confirms its own plan (AGL-3594): the resume
1860
2016
  // door's confirmation, by the job's creator, and the build queued.