@aglyn/plugins-ai 1.0.0-beta.227 → 1.0.0-beta.229

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 (109) hide show
  1. package/README.md +1 -1
  2. package/package.json +17 -17
  3. package/src/lib/components/ai-brief-dialog.component.js +1 -1
  4. package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
  5. package/src/lib/components/ai-describe-automation.component.js +7 -2
  6. package/src/lib/components/ai-describe-automation.component.js.map +1 -1
  7. package/src/lib/components/ai-describe-button.component.js +6 -1
  8. package/src/lib/components/ai-describe-button.component.js.map +1 -1
  9. package/src/lib/components/ai-describe-page.component.d.ts +1 -1
  10. package/src/lib/components/ai-describe-page.component.js +1 -1
  11. package/src/lib/components/ai-describe-page.component.js.map +1 -1
  12. package/src/lib/components/ai-job-events.d.ts +7 -1
  13. package/src/lib/components/ai-job-events.js +4 -2
  14. package/src/lib/components/ai-job-events.js.map +1 -1
  15. package/src/lib/components/ai-job-site.d.ts +22 -0
  16. package/src/lib/components/ai-job-site.js +73 -0
  17. package/src/lib/components/ai-job-site.js.map +1 -0
  18. package/src/lib/components/ai-jobs-list-page.component.d.ts +23 -0
  19. package/src/lib/components/ai-jobs-list-page.component.js +314 -0
  20. package/src/lib/components/ai-jobs-list-page.component.js.map +1 -0
  21. package/src/lib/components/ai-jobs-page.component.d.ts +7 -0
  22. package/src/lib/components/ai-jobs-page.component.js +30 -0
  23. package/src/lib/components/ai-jobs-page.component.js.map +1 -0
  24. package/src/lib/components/ai-page-brief-dialog.component.d.ts +1 -1
  25. package/src/lib/components/ai-page-brief-dialog.component.js.map +1 -1
  26. package/src/lib/components/ai-site-build-page.component.d.ts +31 -3
  27. package/src/lib/components/ai-site-build-page.component.js +394 -243
  28. package/src/lib/components/ai-site-build-page.component.js.map +1 -1
  29. package/src/lib/components/ai-site-start-card.component.d.ts +18 -1
  30. package/src/lib/components/ai-site-start-card.component.js +27 -15
  31. package/src/lib/components/ai-site-start-card.component.js.map +1 -1
  32. package/src/lib/components/assist-jobs-drawer.component.d.ts +11 -0
  33. package/src/lib/components/assist-jobs-drawer.component.js +37 -1
  34. package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
  35. package/src/lib/components/assist-panel.component.js +9 -4
  36. package/src/lib/components/assist-panel.component.js.map +1 -1
  37. package/src/lib/jobs/ai-job-draft-ids.d.ts +6 -0
  38. package/src/lib/jobs/ai-job-draft-ids.js +9 -0
  39. package/src/lib/jobs/ai-job-draft-ids.js.map +1 -1
  40. package/src/lib/jobs/ai-job-drafts.d.ts +7 -0
  41. package/src/lib/jobs/ai-job-drafts.js +16 -7
  42. package/src/lib/jobs/ai-job-drafts.js.map +1 -1
  43. package/src/lib/jobs/ai-job-layout-site-pages.d.ts +70 -0
  44. package/src/lib/jobs/ai-job-layout-site-pages.js +216 -0
  45. package/src/lib/jobs/ai-job-layout-site-pages.js.map +1 -0
  46. package/src/lib/jobs/ai-job-layout-step.d.ts +2 -2
  47. package/src/lib/jobs/ai-job-layout-step.js +21 -2
  48. package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
  49. package/src/lib/jobs/ai-job-page-sections.d.ts +6 -5
  50. package/src/lib/jobs/ai-job-page-sections.js +3 -3
  51. package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
  52. package/src/lib/jobs/ai-job-page-step.d.ts +19 -1
  53. package/src/lib/jobs/ai-job-page-step.js +50 -8
  54. package/src/lib/jobs/ai-job-page-step.js.map +1 -1
  55. package/src/lib/jobs/ai-job-plan-step.js +7 -2
  56. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  57. package/src/lib/jobs/ai-job-site-step.d.ts +21 -0
  58. package/src/lib/jobs/ai-job-site-step.js +103 -17
  59. package/src/lib/jobs/ai-job-site-step.js.map +1 -1
  60. package/src/lib/jobs/ai-job-text-step.d.ts +7 -1
  61. package/src/lib/jobs/ai-job-text-step.js.map +1 -1
  62. package/src/lib/jobs/ai-jobs.d.ts +19 -1
  63. package/src/lib/jobs/ai-jobs.js +130 -53
  64. package/src/lib/jobs/ai-jobs.js.map +1 -1
  65. package/src/lib/jobs/ai-site-publish.d.ts +98 -0
  66. package/src/lib/jobs/ai-site-publish.js +372 -0
  67. package/src/lib/jobs/ai-site-publish.js.map +1 -0
  68. package/src/lib/jobs/fixtures/ai-page-briefs.d.ts +7 -6
  69. package/src/lib/jobs/fixtures/ai-page-briefs.js +23 -56
  70. package/src/lib/jobs/fixtures/ai-page-briefs.js.map +1 -1
  71. package/src/lib/model/ai-job-failure-copy.d.ts +20 -1
  72. package/src/lib/model/ai-job-failure-copy.js +28 -1
  73. package/src/lib/model/ai-job-failure-copy.js.map +1 -1
  74. package/src/lib/model/ai-job-notice.d.ts +6 -1
  75. package/src/lib/model/ai-job-notice.js +29 -3
  76. package/src/lib/model/ai-job-notice.js.map +1 -1
  77. package/src/lib/model/ai-jobs.types.d.ts +34 -0
  78. package/src/lib/model/ai-jobs.types.js.map +1 -1
  79. package/src/lib/model/ai-plan-capabilities.d.ts +25 -10
  80. package/src/lib/model/ai-plan-capabilities.js +15 -2
  81. package/src/lib/model/ai-plan-capabilities.js.map +1 -1
  82. package/src/lib/model/ai-site-build-progress.d.ts +25 -1
  83. package/src/lib/model/ai-site-build-progress.js +129 -15
  84. package/src/lib/model/ai-site-build-progress.js.map +1 -1
  85. package/src/lib/model/ai-site-start-seo.d.ts +35 -0
  86. package/src/lib/model/ai-site-start-seo.js +145 -7
  87. package/src/lib/model/ai-site-start-seo.js.map +1 -1
  88. package/src/lib/model/ai-site-start.d.ts +14 -0
  89. package/src/lib/model/ai-site-start.js +43 -0
  90. package/src/lib/model/ai-site-start.js.map +1 -1
  91. package/src/lib/plugin.js +18 -10
  92. package/src/lib/plugin.js.map +1 -1
  93. package/src/lib/runtime/ai-doctrine-validators.d.ts +16 -12
  94. package/src/lib/runtime/ai-doctrine-validators.js +19 -36
  95. package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
  96. package/src/lib/runtime/ai-doctrine.d.ts +6 -0
  97. package/src/lib/runtime/ai-doctrine.js +6 -2
  98. package/src/lib/runtime/ai-doctrine.js.map +1 -1
  99. package/src/lib/runtime/ai-eval-live.d.ts +1 -1
  100. package/src/lib/runtime/ai-eval-live.js +1 -1
  101. package/src/lib/runtime/ai-eval-live.js.map +1 -1
  102. package/src/lib/server/ai-jobs-live-sites.d.ts +26 -0
  103. package/src/lib/server/ai-jobs-live-sites.js +58 -0
  104. package/src/lib/server/ai-jobs-live-sites.js.map +1 -0
  105. package/src/lib/server/ai-jobs-route.js +22 -3
  106. package/src/lib/server/ai-jobs-route.js.map +1 -1
  107. package/src/lib/usage/assist-job-refund.d.ts +3 -2
  108. package/src/lib/usage/assist-job-refund.js +2 -1
  109. package/src/lib/usage/assist-job-refund.js.map +1 -1
@@ -43,6 +43,8 @@ import { assistExchangeExpiry, assistUsageDay, assistUsageMonth, recordAssistCos
43
43
  import { aiJobRefundCredits, refundJobCredits } from "../usage/assist-job-refund.js";
44
44
  import { freeAssistAccount } from "../usage/assist-free-taste.js";
45
45
  import { aiJobAutoConfirms } from "./ai-job-auto-confirm.js";
46
+ import { AI_SITE_GUIDED_BUILD_FAILED_COPY } from "../model/ai-job-failure-copy.js";
47
+ import { aiSiteStartInputsOf } from "../model/ai-site-start.js";
46
48
  import { addAdminAudit } from "@aglyn/tenant-data-admin/server/admin-audit-write";
47
49
  /**
48
50
  * AI generation jobs — the Firestore state machine (AGL-2904).
@@ -425,14 +427,38 @@ export async function getAiJob(firestore, orgId, jobId) {
425
427
  else if ((_options_statuses = options.statuses) == null ? void 0 : _options_statuses.length) query = query.where('status', 'in', [
426
428
  ...options.statuses
427
429
  ]);
428
- const snapshot = await query.orderBy('createdAt', 'desc').limit(limit).get();
429
- return snapshot.docs.map((doc)=>jobFrom(doc)).filter((job)=>job !== null);
430
+ else if (options.hostId) query = query.where('hostId', '==', options.hostId);
431
+ const read = async (from, count)=>(await from.orderBy('createdAt', 'desc').limit(count).get()).docs.map((doc)=>jobFrom(doc)).filter((job)=>job !== null);
432
+ try {
433
+ return await read(query, limit);
434
+ } catch (error) {
435
+ var _options_statuses1;
436
+ // A site's list while its (hostId, createdAt) index is still building
437
+ // (AGL-3596): Firestore refuses the query with FAILED_PRECONDITION until
438
+ // the index is ready, which answered the site's AI jobs page with a 500.
439
+ // The org's own newest jobs — the index every org list already uses —
440
+ // filtered to the site in memory stand in, bounded to
441
+ // `AI_JOBS_HOST_FALLBACK_SCAN` jobs, so a site whose jobs are older than
442
+ // that lists fewer until the index is ready rather than none.
443
+ if (!options.hostId || options.status || ((_options_statuses1 = options.statuses) == null ? void 0 : _options_statuses1.length) || !isFailedPrecondition(error)) throw error;
444
+ console.warn('ai jobs: host index not ready; listing from the org', {
445
+ orgId,
446
+ hostId: options.hostId
447
+ });
448
+ const recent = await read(jobsCollection(firestore, orgId), AI_JOBS_HOST_FALLBACK_SCAN);
449
+ return recent.filter((job)=>job.hostId === options.hostId).slice(0, limit);
450
+ }
451
+ }
452
+ /** The most of an org's newest jobs a site's list scans while its index builds (AGL-3596). */ export const AI_JOBS_HOST_FALLBACK_SCAN = 300;
453
+ /** Firestore's "this query needs an index that is not ready" (gRPC 9). */ function isFailedPrecondition(error) {
454
+ const code = error == null ? void 0 : error.code;
455
+ return code === 9 || code === 'failed-precondition' || code === 'FAILED_PRECONDITION';
430
456
  }
431
457
  /** The wire form. `running` is the lease, read against `now`. */ export function aiJobSummary(job, now = new Date()) {
432
458
  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;
433
459
  var _job_lease, _job_inputs;
434
460
  const leaseUntil = toMillis((_job_lease = job.lease) == null ? void 0 : _job_lease.until);
435
- return {
461
+ return _extends({
436
462
  id: job.$id,
437
463
  orgId: job.orgId,
438
464
  hostId: (_job_hostId = job.hostId) != null ? _job_hostId : null,
@@ -473,7 +499,11 @@ export async function getAiJob(firestore, orgId, jobId) {
473
499
  versions: (_job_applied_versions = job.applied.versions) != null ? _job_applied_versions : {},
474
500
  staged: (_job_applied_staged = job.applied.staged) != null ? _job_applied_staged : []
475
501
  } : null
476
- };
502
+ }, job.sitePublish ? {
503
+ sitePublish: job.sitePublish
504
+ } : {}, job.kind === 'site' ? {
505
+ siteInputs: aiSiteStartInputsOf(job.inputs)
506
+ } : {});
477
507
  }
478
508
  function leaseIsLive(job, owner, nowMs) {
479
509
  const lease = job.lease;
@@ -688,6 +718,8 @@ function nextStepIndex(job) {
688
718
  lease: null
689
719
  } : {}, input.plan ? {
690
720
  plan: input.plan
721
+ } : {}, input.sitePublish ? {
722
+ sitePublish: input.sitePublish
691
723
  } : {}, parksForReview && input.review ? {
692
724
  review: input.review,
693
725
  error: input.review.reason === 'plan' ? null : input.review.message
@@ -702,10 +734,11 @@ function nextStepIndex(job) {
702
734
  parked: parksForReview && job.status !== 'needs_review'
703
735
  };
704
736
  });
705
- if (recorded.parked) await announceAiJobTransition(recorded.job, 'needs-review');
737
+ if (recorded.parked && !input.quiet) await announceAiJobTransition(recorded.job, 'needs-review');
706
738
  return {
707
739
  job: recorded.job,
708
- remaining: recorded.remaining
740
+ remaining: recorded.remaining,
741
+ parked: recorded.parked
709
742
  };
710
743
  }
711
744
  async function transition(firestore, orgId, jobId, apply) {
@@ -863,6 +896,21 @@ async function transition(firestore, orgId, jobId, apply) {
863
896
  return job;
864
897
  }
865
898
  }
899
+ /**
900
+ * A failure on our side, given back BEFORE the job is failed (AGL-3596): the
901
+ * failed state is the last one a follower of the job reads and the one its
902
+ * notice is written from, so it already carries `refundedCredits`. A job that
903
+ * ended meanwhile — a person's cancel, which pays for what ran — is left as it
904
+ * ended and given nothing back.
905
+ */ async function failOurFailure(firestore, orgId, jobId, message, detail, refund, now) {
906
+ const current = await getAiJob(firestore, orgId, jobId);
907
+ if (current && !isAiJobTerminal(current.status)) {
908
+ await refundOurFailure(firestore, orgId, current, _extends({}, refund, {
909
+ now
910
+ }));
911
+ }
912
+ return failAiJob(firestore, orgId, jobId, message, detail, now);
913
+ }
866
914
  /**
867
915
  * Resume a job that stopped for a person (AGL-2935): confirm its plan, or
868
916
  * try again the step whose answer broke a building rule. One transaction,
@@ -1000,7 +1048,7 @@ async function transition(firestore, orgId, jobId, apply) {
1000
1048
  * round.
1001
1049
  */ export async function runAiJobStep(firestore, orgId, jobId, options) {
1002
1050
  var _options_now, _step_attempts, _options_org, _data, _ref, _ref1, _outcome_effort, _step_passes, _options_actor;
1003
- var _reservation_allotment, _reservation_allotment1;
1051
+ var _reservation_allotment, _reservation_allotment1, _outcome_review;
1004
1052
  const now = (_options_now = options.now) != null ? _options_now : new Date();
1005
1053
  // A reservation the caller took before the job existed is a message
1006
1054
  // counted against the org; a step that fails before the provider is
@@ -1041,32 +1089,28 @@ async function transition(firestore, orgId, jobId, apply) {
1041
1089
  const runner = aiJobRunnerForStep(job.kind, step.name);
1042
1090
  if (!runner) {
1043
1091
  await releaseHeld();
1044
- const failed = await failAiJob(firestore, orgId, jobId, AI_JOB_NOT_AVAILABLE_COPY, {
1045
- stepIndex,
1046
- error: `no runner registered for kind ${job.kind}`
1047
- }, now);
1048
1092
  return {
1049
1093
  outcome: 'failed',
1050
- job: await refundOurFailure(firestore, orgId, failed, {
1094
+ job: await failOurFailure(firestore, orgId, jobId, AI_JOB_NOT_AVAILABLE_COPY, {
1095
+ stepIndex,
1096
+ error: `no runner registered for kind ${job.kind}`
1097
+ }, {
1051
1098
  reason: 'unavailable',
1052
- stepCredits: 0,
1053
- now
1054
- })
1099
+ stepCredits: 0
1100
+ }, now)
1055
1101
  };
1056
1102
  }
1057
1103
  if (((_step_attempts = step.attempts) != null ? _step_attempts : 0) > AI_JOB_STEP_MAX_ATTEMPTS) {
1058
1104
  await releaseHeld();
1059
- const failed = await failAiJob(firestore, orgId, jobId, AI_UPSTREAM_FAILURE_COPY, {
1060
- stepIndex,
1061
- error: `step ${step.name} exhausted ${AI_JOB_STEP_MAX_ATTEMPTS} attempts`
1062
- }, now);
1063
1105
  return {
1064
1106
  outcome: 'failed',
1065
- job: await refundOurFailure(firestore, orgId, failed, {
1107
+ job: await failOurFailure(firestore, orgId, jobId, AI_UPSTREAM_FAILURE_COPY, {
1108
+ stepIndex,
1109
+ error: `step ${step.name} exhausted ${AI_JOB_STEP_MAX_ATTEMPTS} attempts`
1110
+ }, {
1066
1111
  reason: 'timeout',
1067
- stepCredits: 0,
1068
- now
1069
- })
1112
+ stepCredits: 0
1113
+ }, now)
1070
1114
  };
1071
1115
  }
1072
1116
  const org = (_options_org = options.org) != null ? _options_org : (_data = (await firestore.collection('orgs').doc(orgId).get()).data()) != null ? _data : {};
@@ -1192,18 +1236,16 @@ async function transition(firestore, orgId, jobId, apply) {
1192
1236
  job: requeued
1193
1237
  };
1194
1238
  }
1195
- const failed = await failAiJob(firestore, orgId, jobId, AI_UPSTREAM_FAILURE_COPY, {
1196
- stepIndex,
1197
- error
1198
- }, now);
1199
1239
  return {
1200
1240
  outcome: 'failed',
1201
- job: await refundOurFailure(firestore, orgId, failed, {
1241
+ job: await failOurFailure(firestore, orgId, jobId, AI_UPSTREAM_FAILURE_COPY, {
1242
+ stepIndex,
1243
+ error
1244
+ }, {
1202
1245
  reason: 'provider',
1203
1246
  stepCredits: 0,
1204
- free: (_reservation_free = reservation.free) != null ? _reservation_free : null,
1205
- now
1206
- })
1247
+ free: (_reservation_free = reservation.free) != null ? _reservation_free : null
1248
+ }, now)
1207
1249
  };
1208
1250
  }
1209
1251
  const latencyMs = Math.max(0, Date.now() - runStarted);
@@ -1223,18 +1265,16 @@ async function transition(firestore, orgId, jobId, apply) {
1223
1265
  creditsSpent: 0,
1224
1266
  error: outcome.failure
1225
1267
  }, now);
1226
- const failed = await failAiJob(firestore, orgId, jobId, outcome.failure, {
1227
- stepIndex,
1228
- error: `step failure before the provider: ${outcome.failure}`
1229
- }, now);
1230
1268
  return {
1231
1269
  outcome: 'failed',
1232
- job: await refundOurFailure(firestore, orgId, failed, {
1270
+ job: await failOurFailure(firestore, orgId, jobId, outcome.failure, {
1271
+ stepIndex,
1272
+ error: `step failure before the provider: ${outcome.failure}`
1273
+ }, {
1233
1274
  reason: 'step-failure',
1234
1275
  stepCredits: 0,
1235
- free: (_reservation_free1 = reservation.free) != null ? _reservation_free1 : null,
1236
- now
1237
- })
1276
+ free: (_reservation_free1 = reservation.free) != null ? _reservation_free1 : null
1277
+ }, now)
1238
1278
  };
1239
1279
  }
1240
1280
  const tokens = {
@@ -1298,19 +1338,45 @@ async function transition(firestore, orgId, jobId, apply) {
1298
1338
  error: message,
1299
1339
  tokens
1300
1340
  }, now);
1301
- const failed = await failAiJob(firestore, orgId, jobId, message, {
1341
+ const detail = {
1302
1342
  stepIndex,
1303
1343
  error: outcome.refused ? 'stop_reason refusal' : `step failure: ${message}`
1304
- }, now);
1344
+ };
1305
1345
  // A model declining the brief is not our failure; a step's own is (AGL-3594).
1306
1346
  return {
1307
1347
  outcome: 'failed',
1308
- job: outcome.refused ? failed : await refundOurFailure(firestore, orgId, failed, {
1348
+ job: outcome.refused ? await failAiJob(firestore, orgId, jobId, message, detail, now) : await failOurFailure(firestore, orgId, jobId, message, detail, {
1309
1349
  reason: 'step-failure',
1310
1350
  stepCredits: credits,
1311
- free: (_reservation_free3 = reservation.free) != null ? _reservation_free3 : null,
1312
- now
1313
- })
1351
+ free: (_reservation_free3 = reservation.free) != null ? _reservation_free3 : null
1352
+ }, now)
1353
+ };
1354
+ }
1355
+ // A guided site start whose step broke a building rule after its re-ask
1356
+ // fails, given back whole (AGL-3596): the person never saw the plan, so a
1357
+ // review asking them to look at it again is a review of nothing. The rule
1358
+ // and its findings go to the log for staff; the person reads one sentence,
1359
+ // and Try again starts a fresh job.
1360
+ if (((_outcome_review = outcome.review) == null ? void 0 : _outcome_review.reason) === 'doctrine' && aiJobAutoConfirms(job) && step.name !== AI_JOB_PLAN_STEP) {
1361
+ var _outcome_review_findings, _outcome_review_message, _reservation_free4;
1362
+ await recordStep(firestore, orgId, jobId, options.owner, stepIndex, _extends({
1363
+ status: 'failed',
1364
+ creditsSpent: credits,
1365
+ error: AI_SITE_GUIDED_BUILD_FAILED_COPY,
1366
+ outputs: outcome.outputs
1367
+ }, spentNothing ? {} : {
1368
+ tokens
1369
+ }), now);
1370
+ return {
1371
+ outcome: 'failed',
1372
+ job: await failOurFailure(firestore, orgId, jobId, AI_SITE_GUIDED_BUILD_FAILED_COPY, {
1373
+ stepIndex,
1374
+ error: `doctrine refusal after the re-ask: ${JSON.stringify((_outcome_review_findings = outcome.review.findings) != null ? _outcome_review_findings : [])} ${(_outcome_review_message = outcome.review.message) != null ? _outcome_review_message : ''}`
1375
+ }, {
1376
+ reason: 'doctrine-refused',
1377
+ stepCredits: credits,
1378
+ free: (_reservation_free4 = reservation.free) != null ? _reservation_free4 : null
1379
+ }, now)
1314
1380
  };
1315
1381
  }
1316
1382
  // A step that stopped for a person parks the job in the same write that
@@ -1347,6 +1413,10 @@ async function transition(firestore, orgId, jobId, apply) {
1347
1413
  plan: outcome.plan
1348
1414
  } : {}, review ? {
1349
1415
  review
1416
+ } : {}, outcome.sitePublish ? {
1417
+ sitePublish: outcome.sitePublish
1418
+ } : {}, review ? {
1419
+ quiet: true
1350
1420
  } : {}), now);
1351
1421
  const actor = (_options_actor = options.actor) != null ? _options_actor : {
1352
1422
  uid: job.createdBy
@@ -1403,7 +1473,7 @@ async function transition(firestore, orgId, jobId, apply) {
1403
1473
  };
1404
1474
  }
1405
1475
  if (review && recorded.job.status === 'needs_review') {
1406
- var _reservation_free4;
1476
+ var _reservation_free5;
1407
1477
  // Nobody parked the job but its own step, so the row names no actor, as
1408
1478
  // the meter's park does.
1409
1479
  await logAiJobNeedsInput(orgId, {
@@ -1413,15 +1483,22 @@ async function transition(firestore, orgId, jobId, apply) {
1413
1483
  kind: job.kind,
1414
1484
  reason: review.reason
1415
1485
  });
1416
- // A plan the plan rules still refused is our failure (AGL-3594).
1486
+ // A plan the plan rules still refused, and a step that still broke a
1487
+ // building rule after its re-ask, are our failures (AGL-3594, AGL-3596):
1488
+ // given back before the person is told, so the notice can say so. Trying
1489
+ // again spends anew, and a second refusal gives back again, within the
1490
+ // day's bound.
1491
+ const ours = outcome.uncredited ? 'plan-refused' : review.reason === 'doctrine' ? 'doctrine-refused' : null;
1492
+ const parked = ours ? await refundOurFailure(firestore, orgId, recorded.job, {
1493
+ reason: ours,
1494
+ stepCredits: credits,
1495
+ free: (_reservation_free5 = reservation.free) != null ? _reservation_free5 : null,
1496
+ now
1497
+ }) : recorded.job;
1498
+ if (recorded.parked) await announceAiJobTransition(parked, 'needs-review');
1417
1499
  return {
1418
1500
  outcome: 'needs_review',
1419
- job: outcome.uncredited ? await refundOurFailure(firestore, orgId, recorded.job, {
1420
- reason: 'plan-refused',
1421
- stepCredits: credits,
1422
- free: (_reservation_free4 = reservation.free) != null ? _reservation_free4 : null,
1423
- now
1424
- }) : recorded.job
1501
+ job: parked
1425
1502
  };
1426
1503
  }
1427
1504
  if (recorded.remaining > 0 || isAiJobTerminal(recorded.job.status)) {