@aglyn/plugins-ai 1.0.0-beta.226 → 1.0.0-beta.228

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 (203) hide show
  1. package/README.md +3 -2
  2. package/package.json +17 -17
  3. package/src/lib/billing/ai-month-meter.js +5 -3
  4. package/src/lib/billing/ai-month-meter.js.map +1 -1
  5. package/src/lib/billing/ai-overage-ledger.js +3 -1
  6. package/src/lib/billing/ai-overage-ledger.js.map +1 -1
  7. package/src/lib/components/ai-brief-dialog.component.d.ts +6 -2
  8. package/src/lib/components/ai-brief-dialog.component.js +36 -18
  9. package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
  10. package/src/lib/components/ai-describe-automation.component.js +24 -4
  11. package/src/lib/components/ai-describe-automation.component.js.map +1 -1
  12. package/src/lib/components/ai-describe-button.component.js +6 -1
  13. package/src/lib/components/ai-describe-button.component.js.map +1 -1
  14. package/src/lib/components/ai-describe-page.component.d.ts +1 -1
  15. package/src/lib/components/ai-describe-page.component.js +1 -1
  16. package/src/lib/components/ai-describe-page.component.js.map +1 -1
  17. package/src/lib/components/ai-insight-dialog.component.js +24 -2
  18. package/src/lib/components/ai-insight-dialog.component.js.map +1 -1
  19. package/src/lib/components/ai-job-events.d.ts +20 -0
  20. package/src/lib/components/ai-job-events.js +78 -0
  21. package/src/lib/components/ai-job-events.js.map +1 -0
  22. package/src/lib/components/ai-job-follow.component.d.ts +49 -0
  23. package/src/lib/components/ai-job-follow.component.js +210 -0
  24. package/src/lib/components/ai-job-follow.component.js.map +1 -0
  25. package/src/lib/components/ai-job-links.d.ts +42 -0
  26. package/src/lib/components/ai-job-links.js +140 -0
  27. package/src/lib/components/ai-job-links.js.map +1 -0
  28. package/src/lib/components/ai-job-plan.component.d.ts +13 -6
  29. package/src/lib/components/ai-job-plan.component.js +29 -3
  30. package/src/lib/components/ai-job-plan.component.js.map +1 -1
  31. package/src/lib/components/ai-job-requests.d.ts +24 -0
  32. package/src/lib/components/ai-job-requests.js +76 -0
  33. package/src/lib/components/ai-job-requests.js.map +1 -0
  34. package/src/lib/components/ai-job-site.d.ts +22 -0
  35. package/src/lib/components/ai-job-site.js +73 -0
  36. package/src/lib/components/ai-job-site.js.map +1 -0
  37. package/src/lib/components/ai-jobs-indicator.component.d.ts +25 -0
  38. package/src/lib/components/ai-jobs-indicator.component.js +80 -0
  39. package/src/lib/components/ai-jobs-indicator.component.js.map +1 -0
  40. package/src/lib/components/ai-jobs-list-page.component.d.ts +23 -0
  41. package/src/lib/components/ai-jobs-list-page.component.js +303 -0
  42. package/src/lib/components/ai-jobs-list-page.component.js.map +1 -0
  43. package/src/lib/components/ai-jobs-page.component.d.ts +7 -0
  44. package/src/lib/components/ai-jobs-page.component.js +30 -0
  45. package/src/lib/components/ai-jobs-page.component.js.map +1 -0
  46. package/src/lib/components/ai-jobs-store.d.ts +81 -0
  47. package/src/lib/components/ai-jobs-store.js +218 -0
  48. package/src/lib/components/ai-jobs-store.js.map +1 -0
  49. package/src/lib/components/ai-page-brief-dialog.component.d.ts +4 -4
  50. package/src/lib/components/ai-page-brief-dialog.component.js.map +1 -1
  51. package/src/lib/components/ai-site-build-page.component.d.ts +51 -0
  52. package/src/lib/components/ai-site-build-page.component.js +495 -0
  53. package/src/lib/components/ai-site-build-page.component.js.map +1 -0
  54. package/src/lib/components/ai-site-start-card.component.d.ts +9 -1
  55. package/src/lib/components/ai-site-start-card.component.js +533 -132
  56. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  57. package/src/lib/components/ai-site-starter-fallback.component.d.ts +24 -0
  58. package/src/lib/components/ai-site-starter-fallback.component.js +112 -0
  59. package/src/lib/components/ai-site-starter-fallback.component.js.map +1 -0
  60. package/src/lib/components/assist-jobs-drawer.component.d.ts +18 -20
  61. package/src/lib/components/assist-jobs-drawer.component.js +152 -172
  62. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  63. package/src/lib/components/assist-panel.component.js +69 -8
  64. package/src/lib/components/assist-panel.component.js.map +1 -1
  65. package/src/lib/components/staff-ai-credit-return.component.d.ts +55 -0
  66. package/src/lib/components/staff-ai-credit-return.component.js +346 -0
  67. package/src/lib/components/staff-ai-credit-return.component.js.map +1 -0
  68. package/src/lib/components/staff-org-ai-card.component.js +364 -279
  69. package/src/lib/components/staff-org-ai-card.component.js.map +1 -1
  70. package/src/lib/components/staff-user-ai-usage-card.component.d.ts +4 -0
  71. package/src/lib/components/staff-user-ai-usage-card.component.js +83 -28
  72. package/src/lib/components/staff-user-ai-usage-card.component.js.map +1 -1
  73. package/src/lib/components/use-ai-job-run.js +5 -18
  74. package/src/lib/components/use-ai-job-run.js.map +1 -1
  75. package/src/lib/jobs/ai-job-auto-confirm.d.ts +52 -0
  76. package/src/lib/jobs/ai-job-auto-confirm.js +62 -0
  77. package/src/lib/jobs/ai-job-auto-confirm.js.map +1 -0
  78. package/src/lib/jobs/ai-job-draft-ids.d.ts +6 -0
  79. package/src/lib/jobs/ai-job-draft-ids.js +9 -0
  80. package/src/lib/jobs/ai-job-draft-ids.js.map +1 -1
  81. package/src/lib/jobs/ai-job-drafts.d.ts +7 -0
  82. package/src/lib/jobs/ai-job-drafts.js +16 -7
  83. package/src/lib/jobs/ai-job-drafts.js.map +1 -1
  84. package/src/lib/jobs/ai-job-layout-step.js +2 -1
  85. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  86. package/src/lib/jobs/ai-job-page-sections.d.ts +6 -5
  87. package/src/lib/jobs/ai-job-page-sections.js +3 -3
  88. package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
  89. package/src/lib/jobs/ai-job-page-step.d.ts +19 -1
  90. package/src/lib/jobs/ai-job-page-step.js +50 -8
  91. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  92. package/src/lib/jobs/ai-job-plan-step.d.ts +33 -2
  93. package/src/lib/jobs/ai-job-plan-step.js +167 -18
  94. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  95. package/src/lib/jobs/ai-job-site-step.d.ts +28 -0
  96. package/src/lib/jobs/ai-job-site-step.js +130 -21
  97. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  98. package/src/lib/jobs/ai-job-text-step.d.ts +14 -1
  99. package/src/lib/jobs/ai-job-text-step.js.map +1 -1
  100. package/src/lib/jobs/ai-jobs-notify.d.ts +37 -0
  101. package/src/lib/jobs/ai-jobs-notify.js +51 -0
  102. package/src/lib/jobs/ai-jobs-notify.js.map +1 -0
  103. package/src/lib/jobs/ai-jobs.d.ts +39 -4
  104. package/src/lib/jobs/ai-jobs.js +205 -34
  105. package/src/lib/jobs/ai-jobs.js.map +1 -1
  106. package/src/lib/jobs/ai-site-publish.d.ts +98 -0
  107. package/src/lib/jobs/ai-site-publish.js +372 -0
  108. package/src/lib/jobs/ai-site-publish.js.map +1 -0
  109. package/src/lib/jobs/fixtures/ai-page-briefs.d.ts +7 -6
  110. package/src/lib/jobs/fixtures/ai-page-briefs.js +23 -56
  111. package/src/lib/jobs/fixtures/ai-page-briefs.js.map +1 -1
  112. package/src/lib/model/ai-job-activity.d.ts +112 -0
  113. package/src/lib/model/ai-job-activity.js +144 -0
  114. package/src/lib/model/ai-job-activity.js.map +1 -0
  115. package/src/lib/model/ai-job-failure-copy.d.ts +69 -0
  116. package/src/lib/model/ai-job-failure-copy.js +78 -0
  117. package/src/lib/model/ai-job-failure-copy.js.map +1 -0
  118. package/src/lib/model/ai-job-notice.d.ts +63 -0
  119. package/src/lib/model/ai-job-notice.js +129 -0
  120. package/src/lib/model/ai-job-notice.js.map +1 -0
  121. package/src/lib/model/ai-jobs.types.d.ts +53 -0
  122. package/src/lib/model/ai-jobs.types.js +1 -0
  123. package/src/lib/model/ai-jobs.types.js.map +1 -1
  124. package/src/lib/model/ai-plan-capabilities.d.ts +32 -10
  125. package/src/lib/model/ai-plan-capabilities.js +15 -2
  126. package/src/lib/model/ai-plan-capabilities.js.map +1 -1
  127. package/src/lib/model/ai-site-build-progress.d.ts +61 -0
  128. package/src/lib/model/ai-site-build-progress.js +136 -0
  129. package/src/lib/model/ai-site-build-progress.js.map +1 -0
  130. package/src/lib/model/ai-site-inventory.d.ts +9 -0
  131. package/src/lib/model/ai-site-inventory.js +7 -2
  132. package/src/lib/model/ai-site-inventory.js.map +1 -1
  133. package/src/lib/model/ai-site-job.d.ts +93 -2
  134. package/src/lib/model/ai-site-job.js +88 -7
  135. package/src/lib/model/ai-site-job.js.map +1 -1
  136. package/src/lib/model/ai-site-start-seo.d.ts +19 -0
  137. package/src/lib/model/ai-site-start-seo.js +91 -3
  138. package/src/lib/model/ai-site-start-seo.js.map +1 -1
  139. package/src/lib/model/ai-site-start.d.ts +3 -1
  140. package/src/lib/model/ai-site-start.js +6 -4
  141. package/src/lib/model/ai-site-start.js.map +1 -1
  142. package/src/lib/plan-entitlements.d.ts +3 -3
  143. package/src/lib/plan-entitlements.js +3 -3
  144. package/src/lib/plan-entitlements.js.map +1 -1
  145. package/src/lib/plugin.js +38 -2
  146. package/src/lib/plugin.js.map +1 -1
  147. package/src/lib/runtime/ai-abuse-guards.d.ts +9 -6
  148. package/src/lib/runtime/ai-abuse-guards.js +10 -7
  149. package/src/lib/runtime/ai-abuse-guards.js.map +1 -1
  150. package/src/lib/runtime/ai-doctrine-validators.d.ts +16 -12
  151. package/src/lib/runtime/ai-doctrine-validators.js +50 -40
  152. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  153. package/src/lib/runtime/ai-doctrine.js +1 -1
  154. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  155. package/src/lib/runtime/ai-eval-live.d.ts +1 -1
  156. package/src/lib/runtime/ai-eval-live.js +1 -1
  157. package/src/lib/runtime/ai-eval-live.js.map +1 -1
  158. package/src/lib/runtime/ai-gate.d.ts +3 -2
  159. package/src/lib/runtime/ai-gate.js +3 -2
  160. package/src/lib/runtime/ai-gate.js.map +1 -1
  161. package/src/lib/runtime/site-inventory.d.ts +16 -1
  162. package/src/lib/runtime/site-inventory.js +43 -3
  163. package/src/lib/runtime/site-inventory.js.map +1 -1
  164. package/src/lib/server/ai-admin-credits.d.ts +21 -0
  165. package/src/lib/server/ai-admin-credits.js +213 -0
  166. package/src/lib/server/ai-admin-credits.js.map +1 -0
  167. package/src/lib/server/ai-admin-org.js +12 -5
  168. package/src/lib/server/ai-admin-org.js.map +1 -1
  169. package/src/lib/server/ai-admin-signals.js +3 -1
  170. package/src/lib/server/ai-admin-signals.js.map +1 -1
  171. package/src/lib/server/ai-admin-user.js +12 -1
  172. package/src/lib/server/ai-admin-user.js.map +1 -1
  173. package/src/lib/server/ai-allotments.js +3 -2
  174. package/src/lib/server/ai-allotments.js.map +1 -1
  175. package/src/lib/server/ai-jobs-live-sites.d.ts +26 -0
  176. package/src/lib/server/ai-jobs-live-sites.js +58 -0
  177. package/src/lib/server/ai-jobs-live-sites.js.map +1 -0
  178. package/src/lib/server/ai-jobs-route.js +42 -5
  179. package/src/lib/server/ai-jobs-route.js.map +1 -1
  180. package/src/lib/server/assist-chat.js +3 -2
  181. package/src/lib/server/assist-chat.js.map +1 -1
  182. package/src/lib/server/billing-credits.js +4 -3
  183. package/src/lib/server/billing-credits.js.map +1 -1
  184. package/src/lib/server/billing-overage.js +3 -2
  185. package/src/lib/server/billing-overage.js.map +1 -1
  186. package/src/lib/server.js +11 -0
  187. package/src/lib/server.js.map +1 -1
  188. package/src/lib/usage/assist-credit-returns-write.d.ts +75 -0
  189. package/src/lib/usage/assist-credit-returns-write.js +150 -0
  190. package/src/lib/usage/assist-credit-returns-write.js.map +1 -0
  191. package/src/lib/usage/assist-credit-returns.d.ts +68 -0
  192. package/src/lib/usage/assist-credit-returns.js +79 -0
  193. package/src/lib/usage/assist-credit-returns.js.map +1 -0
  194. package/src/lib/usage/assist-free-taste.js +6 -3
  195. package/src/lib/usage/assist-free-taste.js.map +1 -1
  196. package/src/lib/usage/assist-job-refund.d.ts +100 -0
  197. package/src/lib/usage/assist-job-refund.js +109 -0
  198. package/src/lib/usage/assist-job-refund.js.map +1 -0
  199. package/src/lib/usage/assist-usage.js +5 -2
  200. package/src/lib/usage/assist-usage.js.map +1 -1
  201. package/src/lib/usage/staff-org-ai.d.ts +31 -1
  202. package/src/lib/usage/staff-org-ai.js +19 -2
  203. package/src/lib/usage/staff-org-ai.js.map +1 -1
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { notifyUsers } from "@aglyn/tenant-data-admin/server/notifications";
17
+ import { aiJobNotice } from "../model/ai-job-notice.js";
18
+ import { aiJobAutoConfirms } from "./ai-job-auto-confirm.js";
19
+ import { registerAiJobTransitionListener } from "./ai-jobs.js";
20
+ /**
21
+ * The person who started an AI job is told when it needs them, when it
22
+ * finishes and when it stops (AGL-3593), through the console's notifications
23
+ * — the bell, and the generic notification email for a person who switched
24
+ * that channel on for the category. No other channel: there is no push.
25
+ *
26
+ * The machine calls this where the job's state changes, once per change
27
+ * (`registerAiJobTransitionListener`): the request that ran a step inline,
28
+ * or the beat that ran it with nobody watching — which is exactly when a
29
+ * person needs telling. The recipient is the job's creator, whose job it is
30
+ * and who alone confirms its plan; a teammate is not told about someone
31
+ * else's draft.
32
+ *
33
+ * `notifyUsers` never throws, and honors the person's per-category,
34
+ * per-workspace and per-site preferences.
35
+ */ export function aiJobTransitionNotifier(notify = notifyUsers) {
36
+ return async ({ job, to })=>{
37
+ var _job_review;
38
+ if (!job.createdBy) return;
39
+ // A guided site start's plan is confirmed for it the moment it is kept
40
+ // (AGL-3594): there is nothing for the person to confirm.
41
+ if (to === 'needs-review' && ((_job_review = job.review) == null ? void 0 : _job_review.reason) === 'plan' && aiJobAutoConfirms(job)) return;
42
+ await notify([
43
+ job.createdBy
44
+ ], aiJobNotice(job, to));
45
+ };
46
+ }
47
+ /** Registers the notifier the jobs machine tells about each change. */ export function registerAiJobsNotify() {
48
+ registerAiJobTransitionListener(aiJobTransitionNotifier());
49
+ }
50
+
51
+ //# sourceMappingURL=ai-jobs-notify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-jobs-notify.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { notifyUsers } from '@aglyn/tenant-data-admin/server/notifications'\nimport { aiJobNotice } from '../model/ai-job-notice'\nimport { aiJobAutoConfirms } from './ai-job-auto-confirm'\nimport { registerAiJobTransitionListener, type AiJobTransitionListener } from './ai-jobs'\n\n/**\n * The person who started an AI job is told when it needs them, when it\n * finishes and when it stops (AGL-3593), through the console's notifications\n * — the bell, and the generic notification email for a person who switched\n * that channel on for the category. No other channel: there is no push.\n *\n * The machine calls this where the job's state changes, once per change\n * (`registerAiJobTransitionListener`): the request that ran a step inline,\n * or the beat that ran it with nobody watching — which is exactly when a\n * person needs telling. The recipient is the job's creator, whose job it is\n * and who alone confirms its plan; a teammate is not told about someone\n * else's draft.\n *\n * `notifyUsers` never throws, and honors the person's per-category,\n * per-workspace and per-site preferences.\n */\nexport function aiJobTransitionNotifier(\n notify: typeof notifyUsers = notifyUsers,\n): AiJobTransitionListener {\n return async ({ job, to }) => {\n if (!job.createdBy) return\n // A guided site start's plan is confirmed for it the moment it is kept\n // (AGL-3594): there is nothing for the person to confirm.\n if (to === 'needs-review' && job.review?.reason === 'plan' && aiJobAutoConfirms(job)) return\n await notify([job.createdBy], aiJobNotice(job, to))\n }\n}\n\n/** Registers the notifier the jobs machine tells about each change. */\nexport function registerAiJobsNotify(): void {\n registerAiJobTransitionListener(aiJobTransitionNotifier())\n}\n"],"names":["notifyUsers","aiJobNotice","aiJobAutoConfirms","registerAiJobTransitionListener","aiJobTransitionNotifier","notify","job","to","createdBy","review","reason","registerAiJobsNotify"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,WAAW,QAAQ,gDAA+C;AAC3E,SAASC,WAAW,QAAQ,4BAAwB;AACpD,SAASC,iBAAiB,QAAQ,2BAAuB;AACzD,SAASC,+BAA+B,QAAsC,eAAW;AAEzF;;;;;;;;;;;;;;;CAeC,GACD,OAAO,SAASC,wBACdC,SAA6BL,WAAW;IAExC,OAAO,OAAO,EAAEM,GAAG,EAAEC,EAAE,EAAE;YAIMD;QAH7B,IAAI,CAACA,IAAIE,SAAS,EAAE;QACpB,uEAAuE;QACvE,0DAA0D;QAC1D,IAAID,OAAO,kBAAkBD,EAAAA,cAAAA,IAAIG,MAAM,qBAAVH,YAAYI,MAAM,MAAK,UAAUR,kBAAkBI,MAAM;QACtF,MAAMD,OAAO;YAACC,IAAIE,SAAS;SAAC,EAAEP,YAAYK,KAAKC;IACjD;AACF;AAEA,qEAAqE,GACrE,OAAO,SAASI;IACdR,gCAAgCC;AAClC"}
@@ -14,7 +14,7 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
- import { type AiJob, type AiJobApplied, type AiJobKind, type AiJobOutput, type AiJobPlan, type AiJobReview, type AiJobStatus, type AiJobStepTokens, type AiJobSummary } from '../model/ai-jobs.types';
17
+ import { AI_JOB_PLAN_STEP, type AiJob, type AiJobApplied, type AiJobKind, type AiJobOutput, type AiJobPlan, type AiJobReview, type AiJobSitePublish, type AiJobStatus, type AiJobStepTokens, type AiJobSummary } from '../model/ai-jobs.types';
18
18
  import type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types';
19
19
  import { type AiActivityActor } from '../activity/ai-activity';
20
20
  import { type AiEffort, type AiUsage } from '../runtime/ai-runtime';
@@ -107,8 +107,7 @@ export declare const AI_JOB_NOT_AVAILABLE_COPY = "This kind of AI job is not ava
107
107
  export declare const AI_JOB_REFUSED_COPY = "The model declined this brief. Try rephrasing it.";
108
108
  type Firestore = FirebaseFirestore.Firestore;
109
109
  export declare function isAiJobTerminal(status: AiJobStatus): boolean;
110
- /** The step a planned kind runs first (AGL-2935). */
111
- export declare const AI_JOB_PLAN_STEP = "plan";
110
+ export { AI_JOB_PLAN_STEP };
112
111
  /**
113
112
  * The kinds that build site structure, and so plan before they generate
114
113
  * (AGL-2935): the plan their first step proposes is what their generation
@@ -196,6 +195,26 @@ export type AiJobPauseReader = (input: {
196
195
  * runs every job, as a process with no lockdown carrier would.
197
196
  */
198
197
  export declare function registerAiJobPauseReader(reader: AiJobPauseReader | null): void;
198
+ /**
199
+ * A change a person is told about: the job's plan waits for them (or a step
200
+ * stopped for their decision), the job finished, or it stopped.
201
+ */
202
+ export type AiJobTransition = 'needs-review' | 'done' | 'failed';
203
+ export type AiJobTransitionListener = (input: {
204
+ job: AiJob;
205
+ to: AiJobTransition;
206
+ }) => Promise<void> | void;
207
+ /**
208
+ * The listener told each time a job ENTERS `needs_review`, `done` or `failed`
209
+ * — once per entry, from the write that made it, so a stream that re-reads
210
+ * the job or a beat that sees it again tells nobody twice. Registered by its
211
+ * own module (`ai-jobs-notify.ts`) from the console's server surface, as the
212
+ * pause reader is, so this machine never loads the notification fan-out.
213
+ * `null` unregisters it.
214
+ */
215
+ export declare function registerAiJobTransitionListener(listener: AiJobTransitionListener | null): void;
216
+ /** Whether a transition listener is registered: what a surface that runs jobs must have. */
217
+ export declare function aiJobTransitionListenerRegistered(): boolean;
199
218
  /** Whether a pause reader is registered: what a surface that runs jobs must have. */
200
219
  export declare function aiJobPauseReaderRegistered(): boolean;
201
220
  /** The runner for one step of one job: the plan step by its name, else the kind's own. */
@@ -230,9 +249,23 @@ export declare function createAiJob(firestore: Firestore, input: CreateAiJobInpu
230
249
  export declare function getAiJob(firestore: Firestore, orgId: string, jobId: string): Promise<AiJob | null>;
231
250
  export interface ListAiJobsOptions {
232
251
  status?: AiJobStatus;
252
+ /**
253
+ * Any of these statuses (AGL-3593): the console's indicator asks for every
254
+ * job not yet settled in one read. Ignored when `status` is given.
255
+ */
256
+ statuses?: readonly AiJobStatus[];
257
+ /**
258
+ * One site's jobs (AGL-3596), for the site's AI jobs page; on the
259
+ * (hostId, createdAt) index. Not combined with a status filter, which no
260
+ * reader asks for and no index answers.
261
+ */
262
+ hostId?: string;
233
263
  limit?: number;
234
264
  }
235
- /** Newest first. Needs the (status, createdAt) index when filtered. */
265
+ /**
266
+ * Newest first. Needs the (status, createdAt) index when filtered, which
267
+ * answers an `in` over statuses as it answers one status.
268
+ */
236
269
  export declare function listAiJobs(firestore: Firestore, orgId: string, options?: ListAiJobsOptions): Promise<AiJob[]>;
237
270
  /** The wire form. `running` is the lease, read against `now`. */
238
271
  export declare function aiJobSummary(job: AiJob, now?: Date): AiJobSummary;
@@ -288,6 +321,8 @@ export interface RecordStepInput {
288
321
  continued?: boolean;
289
322
  /** What the run cost in tokens and time (AGL-2937), added to the step's totals. */
290
323
  tokens?: AiJobStepTokenRun;
324
+ /** What a guided site start put live (AGL-3596), kept on the job. */
325
+ sitePublish?: AiJobSitePublish;
291
326
  }
292
327
  /** One run of a step's runner, as the machine measured it (AGL-2937). */
293
328
  export interface AiJobStepTokenRun {
@@ -23,7 +23,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
23
23
  import { createResourceUid } from "@aglyn/aglyn/app-utils/create-resource-uid";
24
24
  import { checkEntitlement } from "@aglyn/aglyn/server";
25
25
  import { assistCreditsFromUsd, assistFreeTasteRefusalText, assistHardCapRefusalText, assistRefusedByHardCap } from "../usage/assist-credits.js";
26
- import { AI_JOB_TERMINAL_STATUSES } from "../model/ai-jobs.types.js";
26
+ import { AI_JOB_PLAN_STEP, AI_JOB_TERMINAL_STATUSES } from "../model/ai-jobs.types.js";
27
27
  import { resolveEffectivePlan } from "@aglyn/aglyn/app-utils/plan-entitlements";
28
28
  import { aiOverageReservationRefusal } from "../billing/ai-overage-gate.js";
29
29
  import { aiAllotmentRefusalText } from "../model/ai-allotments.js";
@@ -39,7 +39,10 @@ import { aiMintJobDraftIds } from "./ai-job-draft-ids.js";
39
39
  import { AI_JOB_TEXT_STEP_MINIMUM_MS, runAiJobTextStep } from "./ai-job-text-step.js";
40
40
  import { AI_JOB_SEO_STEP_MINIMUM_MS } from "./ai-job-seo-budget.js";
41
41
  import { AI_JOB_THEME_STEP_MINIMUM_MS } from "./ai-job-theme-budget.js";
42
- import { assistExchangeExpiry, recordAssistCost, releaseAssistMessage, reserveAssistMessage } from "../usage/assist-usage.js";
42
+ import { assistExchangeExpiry, assistUsageDay, assistUsageMonth, recordAssistCost, releaseAssistMessage, reserveAssistMessage } from "../usage/assist-usage.js";
43
+ import { aiJobRefundCredits, refundJobCredits } from "../usage/assist-job-refund.js";
44
+ import { freeAssistAccount } from "../usage/assist-free-taste.js";
45
+ import { aiJobAutoConfirms } from "./ai-job-auto-confirm.js";
43
46
  import { addAdminAudit } from "@aglyn/tenant-data-admin/server/admin-audit-write";
44
47
  /**
45
48
  * AI generation jobs — the Firestore state machine (AGL-2904).
@@ -134,7 +137,9 @@ export function isAiJobTerminal(status) {
134
137
  return AI_JOB_TERMINAL_STATUSES.includes(status);
135
138
  }
136
139
  // ── The step runner registry ──────────────────────────────────────────────
137
- /** The step a planned kind runs first (AGL-2935). */ export const AI_JOB_PLAN_STEP = 'plan';
140
+ // The step a planned kind runs first (AGL-2935), named in the model so the
141
+ // console reads a job's phase off its steps without loading this machine.
142
+ export { AI_JOB_PLAN_STEP };
138
143
  /**
139
144
  * The kinds that build site structure, and so plan before they generate
140
145
  * (AGL-2935): the plan their first step proposes is what their generation
@@ -232,6 +237,36 @@ let pauseReader = null;
232
237
  */ export function registerAiJobPauseReader(reader) {
233
238
  pauseReader = reader;
234
239
  }
240
+ let transitionListener = null;
241
+ /**
242
+ * The listener told each time a job ENTERS `needs_review`, `done` or `failed`
243
+ * — once per entry, from the write that made it, so a stream that re-reads
244
+ * the job or a beat that sees it again tells nobody twice. Registered by its
245
+ * own module (`ai-jobs-notify.ts`) from the console's server surface, as the
246
+ * pause reader is, so this machine never loads the notification fan-out.
247
+ * `null` unregisters it.
248
+ */ export function registerAiJobTransitionListener(listener) {
249
+ transitionListener = listener;
250
+ }
251
+ /** Whether a transition listener is registered: what a surface that runs jobs must have. */ export function aiJobTransitionListenerRegistered() {
252
+ return transitionListener !== null;
253
+ }
254
+ /** Tells the listener; a listener that throws never fails the write it follows. */ async function announceAiJobTransition(job, to) {
255
+ if (!transitionListener) return;
256
+ try {
257
+ await transitionListener({
258
+ job,
259
+ to
260
+ });
261
+ } catch (error) {
262
+ console.error('ai job transition listener failed', {
263
+ orgId: job.orgId,
264
+ jobId: job.$id,
265
+ to,
266
+ error
267
+ });
268
+ }
269
+ }
235
270
  /** Whether a pause reader is registered: what a surface that runs jobs must have. */ export function aiJobPauseReaderRegistered() {
236
271
  return pauseReader !== null;
237
272
  }
@@ -378,19 +413,27 @@ function jobFrom(snapshot) {
378
413
  export async function getAiJob(firestore, orgId, jobId) {
379
414
  return jobFrom(await jobsCollection(firestore, orgId).doc(jobId).get());
380
415
  }
381
- /** Newest first. Needs the (status, createdAt) index when filtered. */ export async function listAiJobs(firestore, orgId, options = {}) {
416
+ /**
417
+ * Newest first. Needs the (status, createdAt) index when filtered, which
418
+ * answers an `in` over statuses as it answers one status.
419
+ */ export async function listAiJobs(firestore, orgId, options = {}) {
382
420
  var _options_limit;
421
+ var _options_statuses;
383
422
  const limit = Math.min(Math.max(1, Math.floor((_options_limit = options.limit) != null ? _options_limit : 20)), 100);
384
423
  let query = jobsCollection(firestore, orgId);
385
424
  if (options.status) query = query.where('status', '==', options.status);
425
+ else if ((_options_statuses = options.statuses) == null ? void 0 : _options_statuses.length) query = query.where('status', 'in', [
426
+ ...options.statuses
427
+ ]);
428
+ else if (options.hostId) query = query.where('hostId', '==', options.hostId);
386
429
  const snapshot = await query.orderBy('createdAt', 'desc').limit(limit).get();
387
430
  return snapshot.docs.map((doc)=>jobFrom(doc)).filter((job)=>job !== null);
388
431
  }
389
432
  /** The wire form. `running` is the lease, read against `now`. */ export function aiJobSummary(job, now = new Date()) {
390
- var _job_hostId, _job_steps, _job_outputs, _job_creditsReserved, _job_creditsSpent, _toIso, _toIso1, _job_error, _job_plan_labels, _job_plan_confirmedBy, _job_review, _job_applied_versions, _job_applied_staged;
433
+ var _job_hostId, _job_steps, _job_outputs, _job_creditsReserved, _job_creditsSpent, _job_refundedCredits, _toIso, _toIso1, _job_error, _job_plan_labels, _job_plan_confirmedBy, _job_review, _job_applied_versions, _job_applied_staged;
391
434
  var _job_lease, _job_inputs;
392
435
  const leaseUntil = toMillis((_job_lease = job.lease) == null ? void 0 : _job_lease.until);
393
- return {
436
+ return _extends({
394
437
  id: job.$id,
395
438
  orgId: job.orgId,
396
439
  hostId: (_job_hostId = job.hostId) != null ? _job_hostId : null,
@@ -412,6 +455,7 @@ export async function getAiJob(firestore, orgId, jobId) {
412
455
  outputs: (_job_outputs = job.outputs) != null ? _job_outputs : [],
413
456
  creditsReserved: (_job_creditsReserved = job.creditsReserved) != null ? _job_creditsReserved : 0,
414
457
  creditsSpent: (_job_creditsSpent = job.creditsSpent) != null ? _job_creditsSpent : 0,
458
+ refundedCredits: (_job_refundedCredits = job.refundedCredits) != null ? _job_refundedCredits : 0,
415
459
  createdBy: job.createdBy,
416
460
  createdAt: (_toIso = toIso(job.createdAt)) != null ? _toIso : new Date(0).toISOString(),
417
461
  updatedAt: (_toIso1 = toIso(job.updatedAt)) != null ? _toIso1 : new Date(0).toISOString(),
@@ -430,7 +474,9 @@ export async function getAiJob(firestore, orgId, jobId) {
430
474
  versions: (_job_applied_versions = job.applied.versions) != null ? _job_applied_versions : {},
431
475
  staged: (_job_applied_staged = job.applied.staged) != null ? _job_applied_staged : []
432
476
  } : null
433
- };
477
+ }, job.sitePublish ? {
478
+ sitePublish: job.sitePublish
479
+ } : {});
434
480
  }
435
481
  function leaseIsLive(job, owner, nowMs) {
436
482
  const lease = job.lease;
@@ -605,7 +651,7 @@ function nextStepIndex(job) {
605
651
  * in the same transaction, so no beat can claim the next step in between.
606
652
  */ export async function recordStep(firestore, orgId, jobId, owner, stepIndex, input, now = new Date()) {
607
653
  const ref = jobsCollection(firestore, orgId).doc(jobId);
608
- return firestore.runTransaction(async (tx)=>{
654
+ const recorded = await firestore.runTransaction(async (tx)=>{
609
655
  var _job_outputs, _input_outputs, _job_creditsSpent, _job_creditsReserved, _job_creditsReserved1;
610
656
  var _job_lease;
611
657
  const job = jobFrom(await tx.get(ref));
@@ -645,6 +691,8 @@ function nextStepIndex(job) {
645
691
  lease: null
646
692
  } : {}, input.plan ? {
647
693
  plan: input.plan
694
+ } : {}, input.sitePublish ? {
695
+ sitePublish: input.sitePublish
648
696
  } : {}, parksForReview && input.review ? {
649
697
  review: input.review,
650
698
  error: input.review.reason === 'plan' ? null : input.review.message
@@ -654,9 +702,16 @@ function nextStepIndex(job) {
654
702
  });
655
703
  return {
656
704
  job: _extends({}, job, patch),
657
- remaining
705
+ remaining,
706
+ // Entered `needs_review` in this write, rather than parked again.
707
+ parked: parksForReview && job.status !== 'needs_review'
658
708
  };
659
709
  });
710
+ if (recorded.parked) await announceAiJobTransition(recorded.job, 'needs-review');
711
+ return {
712
+ job: recorded.job,
713
+ remaining: recorded.remaining
714
+ };
660
715
  }
661
716
  async function transition(firestore, orgId, jobId, apply) {
662
717
  const ref = jobsCollection(firestore, orgId).doc(jobId);
@@ -678,13 +733,14 @@ async function transition(firestore, orgId, jobId, apply) {
678
733
  });
679
734
  }
680
735
  /** Every step done: the job is `done` and holds nothing. */ export async function completeAiJob(firestore, orgId, jobId, now = new Date()) {
681
- const { job } = await transition(firestore, orgId, jobId, (current)=>isAiJobTerminal(current.status) ? null : {
736
+ const { job, changed } = await transition(firestore, orgId, jobId, (current)=>isAiJobTerminal(current.status) ? null : {
682
737
  status: 'done',
683
738
  creditsReserved: 0,
684
739
  lease: null,
685
740
  error: null,
686
741
  updatedAt: now
687
742
  });
743
+ if (changed) await announceAiJobTransition(job, 'done');
688
744
  return job;
689
745
  }
690
746
  /**
@@ -699,7 +755,7 @@ async function transition(firestore, orgId, jobId, apply) {
699
755
  stepIndex: (_ref = detail == null ? void 0 : detail.stepIndex) != null ? _ref : null,
700
756
  error: (detail == null ? void 0 : detail.error) instanceof Error ? detail.error.message : (_ref1 = detail == null ? void 0 : detail.error) != null ? _ref1 : null
701
757
  });
702
- const { job } = await transition(firestore, orgId, jobId, (current)=>isAiJobTerminal(current.status) ? null : {
758
+ const { job, changed } = await transition(firestore, orgId, jobId, (current)=>isAiJobTerminal(current.status) ? null : {
703
759
  status: 'failed',
704
760
  error: message,
705
761
  creditsReserved: 0,
@@ -711,6 +767,7 @@ async function transition(firestore, orgId, jobId, apply) {
711
767
  }) : step),
712
768
  updatedAt: now
713
769
  });
770
+ if (changed) await announceAiJobTransition(job, 'failed');
714
771
  return job;
715
772
  }
716
773
  /**
@@ -760,6 +817,57 @@ async function transition(firestore, orgId, jobId, apply) {
760
817
  }
761
818
  return result;
762
819
  }
820
+ /**
821
+ * Gives back what a job spent when it failed on our side (AGL-3594), and
822
+ * records the give-back on the job — `refundedCredits`, `refundReason` and
823
+ * the count its next key is numbered by. Everything the job spent and has not
824
+ * given back where it delivered nothing; the failing step's own spend where
825
+ * it delivered drafts, which it keeps. `assist-job-refund.ts` has the rule
826
+ * and the bound. Never throws: a give-back that cannot be made leaves the job
827
+ * as the failure left it, metered as every other step.
828
+ */ async function refundOurFailure(firestore, orgId, job, input) {
829
+ try {
830
+ var _job_outputs, _job_creditsSpent, _job_refundedCredits, _data, _job_refunds;
831
+ const delivered = ((_job_outputs = job.outputs) != null ? _job_outputs : []).some((output)=>output.resource !== 'seo');
832
+ const credits = aiJobRefundCredits({
833
+ delivered,
834
+ jobCredits: (_job_creditsSpent = job.creditsSpent) != null ? _job_creditsSpent : 0,
835
+ stepCredits: input.stepCredits,
836
+ alreadyRefunded: (_job_refundedCredits = job.refundedCredits) != null ? _job_refundedCredits : 0
837
+ });
838
+ if (credits <= 0) return job;
839
+ const free = input.free !== undefined ? input.free : freeAssistAccount((_data = (await firestore.collection('orgs').doc(orgId).get()).data()) != null ? _data : {});
840
+ const ordinal = (_job_refunds = job.refunds) != null ? _job_refunds : 0;
841
+ const refund = await refundJobCredits(firestore, {
842
+ orgId,
843
+ free,
844
+ jobId: job.$id,
845
+ ordinal,
846
+ credits,
847
+ month: assistUsageMonth(input.now),
848
+ day: assistUsageDay(input.now),
849
+ reason: input.reason
850
+ });
851
+ if (refund.status !== 'returned') return job;
852
+ const { job: refunded } = await transition(firestore, orgId, job.$id, (current)=>{
853
+ var _current_refundedCredits, _current_refunds;
854
+ return {
855
+ refundedCredits: ((_current_refundedCredits = current.refundedCredits) != null ? _current_refundedCredits : 0) + credits,
856
+ refundReason: input.reason,
857
+ refunds: ((_current_refunds = current.refunds) != null ? _current_refunds : 0) + 1,
858
+ updatedAt: input.now
859
+ };
860
+ });
861
+ return refunded;
862
+ } catch (error) {
863
+ console.error('ai job refund failed', {
864
+ orgId,
865
+ jobId: job.$id,
866
+ error
867
+ });
868
+ return job;
869
+ }
870
+ }
763
871
  /**
764
872
  * Resume a job that stopped for a person (AGL-2935): confirm its plan, or
765
873
  * try again the step whose answer broke a building rule. One transaction,
@@ -938,22 +1046,32 @@ async function transition(firestore, orgId, jobId, apply) {
938
1046
  const runner = aiJobRunnerForStep(job.kind, step.name);
939
1047
  if (!runner) {
940
1048
  await releaseHeld();
1049
+ const failed = await failAiJob(firestore, orgId, jobId, AI_JOB_NOT_AVAILABLE_COPY, {
1050
+ stepIndex,
1051
+ error: `no runner registered for kind ${job.kind}`
1052
+ }, now);
941
1053
  return {
942
1054
  outcome: 'failed',
943
- job: await failAiJob(firestore, orgId, jobId, AI_JOB_NOT_AVAILABLE_COPY, {
944
- stepIndex,
945
- error: `no runner registered for kind ${job.kind}`
946
- }, now)
1055
+ job: await refundOurFailure(firestore, orgId, failed, {
1056
+ reason: 'unavailable',
1057
+ stepCredits: 0,
1058
+ now
1059
+ })
947
1060
  };
948
1061
  }
949
1062
  if (((_step_attempts = step.attempts) != null ? _step_attempts : 0) > AI_JOB_STEP_MAX_ATTEMPTS) {
950
1063
  await releaseHeld();
1064
+ const failed = await failAiJob(firestore, orgId, jobId, AI_UPSTREAM_FAILURE_COPY, {
1065
+ stepIndex,
1066
+ error: `step ${step.name} exhausted ${AI_JOB_STEP_MAX_ATTEMPTS} attempts`
1067
+ }, now);
951
1068
  return {
952
1069
  outcome: 'failed',
953
- job: await failAiJob(firestore, orgId, jobId, AI_UPSTREAM_FAILURE_COPY, {
954
- stepIndex,
955
- error: `step ${step.name} exhausted ${AI_JOB_STEP_MAX_ATTEMPTS} attempts`
956
- }, now)
1070
+ job: await refundOurFailure(firestore, orgId, failed, {
1071
+ reason: 'timeout',
1072
+ stepCredits: 0,
1073
+ now
1074
+ })
957
1075
  };
958
1076
  }
959
1077
  const org = (_options_org = options.org) != null ? _options_org : (_data = (await firestore.collection('orgs').doc(orgId).get()).data()) != null ? _data : {};
@@ -1058,6 +1176,7 @@ async function transition(firestore, orgId, jobId, apply) {
1058
1176
  modelFor
1059
1177
  });
1060
1178
  } catch (error) {
1179
+ var _reservation_free;
1061
1180
  // The provider refused the request or the budget ended before it
1062
1181
  // answered: no usage came back, so nothing is metered and the message
1063
1182
  // goes back to the org. The provider's own words are already in the
@@ -1078,12 +1197,18 @@ async function transition(firestore, orgId, jobId, apply) {
1078
1197
  job: requeued
1079
1198
  };
1080
1199
  }
1200
+ const failed = await failAiJob(firestore, orgId, jobId, AI_UPSTREAM_FAILURE_COPY, {
1201
+ stepIndex,
1202
+ error
1203
+ }, now);
1081
1204
  return {
1082
1205
  outcome: 'failed',
1083
- job: await failAiJob(firestore, orgId, jobId, AI_UPSTREAM_FAILURE_COPY, {
1084
- stepIndex,
1085
- error
1086
- }, now)
1206
+ job: await refundOurFailure(firestore, orgId, failed, {
1207
+ reason: 'provider',
1208
+ stepCredits: 0,
1209
+ free: (_reservation_free = reservation.free) != null ? _reservation_free : null,
1210
+ now
1211
+ })
1087
1212
  };
1088
1213
  }
1089
1214
  const latencyMs = Math.max(0, Date.now() - runStarted);
@@ -1092,6 +1217,7 @@ async function transition(firestore, orgId, jobId, apply) {
1092
1217
  // back and nothing is metered, exactly as for a request the provider
1093
1218
  // refused.
1094
1219
  if (outcome.failure && aiJobStepSpentNothing(outcome)) {
1220
+ var _reservation_free1;
1095
1221
  await releaseAssistMessage(firestore, orgId, reservation).catch((releaseError)=>console.error('ai job release failed', {
1096
1222
  orgId,
1097
1223
  jobId,
@@ -1102,12 +1228,18 @@ async function transition(firestore, orgId, jobId, apply) {
1102
1228
  creditsSpent: 0,
1103
1229
  error: outcome.failure
1104
1230
  }, now);
1231
+ const failed = await failAiJob(firestore, orgId, jobId, outcome.failure, {
1232
+ stepIndex,
1233
+ error: `step failure before the provider: ${outcome.failure}`
1234
+ }, now);
1105
1235
  return {
1106
1236
  outcome: 'failed',
1107
- job: await failAiJob(firestore, orgId, jobId, outcome.failure, {
1108
- stepIndex,
1109
- error: `step failure before the provider: ${outcome.failure}`
1110
- }, now)
1237
+ job: await refundOurFailure(firestore, orgId, failed, {
1238
+ reason: 'step-failure',
1239
+ stepCredits: 0,
1240
+ free: (_reservation_free1 = reservation.free) != null ? _reservation_free1 : null,
1241
+ now
1242
+ })
1111
1243
  };
1112
1244
  }
1113
1245
  const tokens = {
@@ -1132,7 +1264,7 @@ async function transition(firestore, orgId, jobId, apply) {
1132
1264
  // Tokens were spent, so the bill is written first and its failure is a
1133
1265
  // log line, never a failed job — the same rule the chat route keeps.
1134
1266
  try {
1135
- var _job_hostId1, _reservation_free;
1267
+ var _job_hostId1, _reservation_free2;
1136
1268
  await recordAssistCost(firestore, orgId, {
1137
1269
  route: 'ai/jobs',
1138
1270
  hostId: (_job_hostId1 = job.hostId) != null ? _job_hostId1 : null,
@@ -1145,7 +1277,7 @@ async function transition(firestore, orgId, jobId, apply) {
1145
1277
  // The account this job drew on, as the reservation decided it
1146
1278
  // (AGL-2925): a Free job lands on the owner's allowance and the
1147
1279
  // platform's day; a paid one on neither.
1148
- free: (_reservation_free = reservation.free) != null ? _reservation_free : null,
1280
+ free: (_reservation_free2 = reservation.free) != null ? _reservation_free2 : null,
1149
1281
  // The step is the creator's spend, under the job's kind (AGL-2928).
1150
1282
  uid: job.createdBy,
1151
1283
  kind: job.kind
@@ -1163,6 +1295,7 @@ async function transition(firestore, orgId, jobId, apply) {
1163
1295
  // model's answer (AGL-2938), fails the job with its own sentence. The
1164
1296
  // tokens are already on the bill above.
1165
1297
  if (outcome.refused || outcome.failure) {
1298
+ var _reservation_free3;
1166
1299
  const message = outcome.refused ? AI_JOB_REFUSED_COPY : outcome.failure;
1167
1300
  await recordStep(firestore, orgId, jobId, options.owner, stepIndex, {
1168
1301
  status: 'failed',
@@ -1170,12 +1303,19 @@ async function transition(firestore, orgId, jobId, apply) {
1170
1303
  error: message,
1171
1304
  tokens
1172
1305
  }, now);
1306
+ const failed = await failAiJob(firestore, orgId, jobId, message, {
1307
+ stepIndex,
1308
+ error: outcome.refused ? 'stop_reason refusal' : `step failure: ${message}`
1309
+ }, now);
1310
+ // A model declining the brief is not our failure; a step's own is (AGL-3594).
1173
1311
  return {
1174
1312
  outcome: 'failed',
1175
- job: await failAiJob(firestore, orgId, jobId, message, {
1176
- stepIndex,
1177
- error: outcome.refused ? 'stop_reason refusal' : `step failure: ${message}`
1178
- }, now)
1313
+ job: outcome.refused ? failed : await refundOurFailure(firestore, orgId, failed, {
1314
+ reason: 'step-failure',
1315
+ stepCredits: credits,
1316
+ free: (_reservation_free3 = reservation.free) != null ? _reservation_free3 : null,
1317
+ now
1318
+ })
1179
1319
  };
1180
1320
  }
1181
1321
  // A step that stopped for a person parks the job in the same write that
@@ -1212,6 +1352,8 @@ async function transition(firestore, orgId, jobId, apply) {
1212
1352
  plan: outcome.plan
1213
1353
  } : {}, review ? {
1214
1354
  review
1355
+ } : {}, outcome.sitePublish ? {
1356
+ sitePublish: outcome.sitePublish
1215
1357
  } : {}), now);
1216
1358
  const actor = (_options_actor = options.actor) != null ? _options_actor : {
1217
1359
  uid: job.createdBy
@@ -1245,7 +1387,30 @@ async function transition(firestore, orgId, jobId, apply) {
1245
1387
  }
1246
1388
  });
1247
1389
  }
1390
+ if ((review == null ? void 0 : review.reason) === 'plan' && recorded.job.status === 'needs_review' && aiJobAutoConfirms(job)) {
1391
+ // A guided site start confirms its own plan (AGL-3594): the resume
1392
+ // door's confirmation, by the job's creator, and the build queued.
1393
+ const resumed = await resumeAiJob(firestore, orgId, jobId, {
1394
+ uid: job.createdBy
1395
+ }, now);
1396
+ await writeAiJobAudit(firestore, {
1397
+ action: 'ai.job.resume',
1398
+ actorUid: job.createdBy,
1399
+ orgId,
1400
+ jobId,
1401
+ after: {
1402
+ status: resumed.job.status,
1403
+ confirmedPlan: true,
1404
+ autoConfirm: true
1405
+ }
1406
+ });
1407
+ return {
1408
+ outcome: 'done',
1409
+ job: resumed.job
1410
+ };
1411
+ }
1248
1412
  if (review && recorded.job.status === 'needs_review') {
1413
+ var _reservation_free4;
1249
1414
  // Nobody parked the job but its own step, so the row names no actor, as
1250
1415
  // the meter's park does.
1251
1416
  await logAiJobNeedsInput(orgId, {
@@ -1255,9 +1420,15 @@ async function transition(firestore, orgId, jobId, apply) {
1255
1420
  kind: job.kind,
1256
1421
  reason: review.reason
1257
1422
  });
1423
+ // A plan the plan rules still refused is our failure (AGL-3594).
1258
1424
  return {
1259
1425
  outcome: 'needs_review',
1260
- job: recorded.job
1426
+ job: outcome.uncredited ? await refundOurFailure(firestore, orgId, recorded.job, {
1427
+ reason: 'plan-refused',
1428
+ stepCredits: credits,
1429
+ free: (_reservation_free4 = reservation.free) != null ? _reservation_free4 : null,
1430
+ now
1431
+ }) : recorded.job
1261
1432
  };
1262
1433
  }
1263
1434
  if (recorded.remaining > 0 || isAiJobTerminal(recorded.job.status)) {