@aglyn/plugins-ai 1.0.0-beta.237 → 1.0.0-beta.238
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -17
- package/src/lib/components/ai-credits-prompt.component.d.ts +11 -4
- package/src/lib/components/ai-credits-prompt.component.js +84 -34
- package/src/lib/components/ai-credits-prompt.component.js.map +1 -1
- package/src/lib/components/ai-job-cancel.component.d.ts +38 -0
- package/src/lib/components/ai-job-cancel.component.js +156 -0
- package/src/lib/components/ai-job-cancel.component.js.map +1 -0
- package/src/lib/components/ai-job-plan.component.js +12 -1
- package/src/lib/components/ai-job-plan.component.js.map +1 -1
- package/src/lib/components/ai-jobs-list-page.component.js +141 -72
- package/src/lib/components/ai-jobs-list-page.component.js.map +1 -1
- package/src/lib/components/ai-site-build-page.component.js +22 -2
- package/src/lib/components/ai-site-build-page.component.js.map +1 -1
- package/src/lib/components/ai-usage-strip.component.d.ts +19 -2
- package/src/lib/components/ai-usage-strip.component.js +40 -9
- package/src/lib/components/ai-usage-strip.component.js.map +1 -1
- package/src/lib/components/assist-jobs-drawer.component.js +12 -16
- package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
- package/src/lib/components/assist-panel.component.js +87 -51
- package/src/lib/components/assist-panel.component.js.map +1 -1
- package/src/lib/jobs/ai-job-build-step.js +4 -1
- package/src/lib/jobs/ai-job-build-step.js.map +1 -1
- package/src/lib/jobs/ai-job-cancel.d.ts +71 -0
- package/src/lib/jobs/ai-job-cancel.js +102 -0
- package/src/lib/jobs/ai-job-cancel.js.map +1 -0
- package/src/lib/jobs/ai-job-component-step.js +4 -1
- package/src/lib/jobs/ai-job-component-step.js.map +1 -1
- package/src/lib/jobs/ai-job-drafts.js +2 -0
- package/src/lib/jobs/ai-job-drafts.js.map +1 -1
- package/src/lib/jobs/ai-job-form-step.js +4 -1
- package/src/lib/jobs/ai-job-form-step.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-language.d.ts +2 -0
- package/src/lib/jobs/ai-job-layout-language.js +13 -1
- package/src/lib/jobs/ai-job-layout-language.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-step.js +6 -1
- package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-language.js +8 -0
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.js +12 -1
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +20 -1
- package/src/lib/jobs/ai-job-plan-step.js +49 -9
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-content.d.ts +15 -0
- package/src/lib/jobs/ai-job-site-content.js +66 -7
- package/src/lib/jobs/ai-job-site-content.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.js +4 -1
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-job-template-step.js +4 -1
- package/src/lib/jobs/ai-job-template-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs.d.ts +37 -12
- package/src/lib/jobs/ai-jobs.js +184 -28
- package/src/lib/jobs/ai-jobs.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +143 -70
- package/src/lib/jobs/ai-layout-stock-photos.js +649 -176
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.d.ts +30 -0
- package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.js +220 -0
- package/src/lib/jobs/fixtures/ai-ceramics-portfolio-site-plan.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.d.ts +27 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.js +151 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-pixabay-recordings.js.map +1 -0
- package/src/lib/layout-language/ai-layout-compiler.d.ts +6 -0
- package/src/lib/layout-language/ai-layout-compiler.js +304 -8
- package/src/lib/layout-language/ai-layout-compiler.js.map +1 -1
- package/src/lib/layout-language/ai-layout-design.d.ts +6 -3
- package/src/lib/layout-language/ai-layout-design.js +5 -3
- package/src/lib/layout-language/ai-layout-design.js.map +1 -1
- package/src/lib/layout-language/ai-layout-frame.d.ts +12 -0
- package/src/lib/layout-language/ai-layout-frame.js +58 -3
- package/src/lib/layout-language/ai-layout-frame.js.map +1 -1
- package/src/lib/layout-language/ai-layout-listings.d.ts +13 -1
- package/src/lib/layout-language/ai-layout-listings.js +61 -46
- package/src/lib/layout-language/ai-layout-listings.js.map +1 -1
- package/src/lib/layout-language/ai-layout-pictures.d.ts +8 -0
- package/src/lib/layout-language/ai-layout-pictures.js.map +1 -1
- package/src/lib/layout-language/ai-layout-storefront.d.ts +82 -0
- package/src/lib/layout-language/ai-layout-storefront.js +223 -0
- package/src/lib/layout-language/ai-layout-storefront.js.map +1 -0
- package/src/lib/model/ai-build-job.d.ts +9 -1
- package/src/lib/model/ai-build-job.js +19 -5
- package/src/lib/model/ai-build-job.js.map +1 -1
- package/src/lib/model/ai-build-progress.d.ts +2 -2
- package/src/lib/model/ai-build-progress.js +6 -4
- package/src/lib/model/ai-build-progress.js.map +1 -1
- package/src/lib/model/ai-credit-estimate.d.ts +69 -2
- package/src/lib/model/ai-credit-estimate.js +91 -3
- package/src/lib/model/ai-credit-estimate.js.map +1 -1
- package/src/lib/model/ai-job-cancel-copy.d.ts +44 -0
- package/src/lib/model/ai-job-cancel-copy.js +53 -0
- package/src/lib/model/ai-job-cancel-copy.js.map +1 -0
- package/src/lib/model/ai-job-failure-copy.d.ts +4 -1
- package/src/lib/model/ai-job-failure-copy.js +8 -3
- package/src/lib/model/ai-job-failure-copy.js.map +1 -1
- package/src/lib/model/ai-jobs.types.d.ts +20 -0
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-site-build-progress.js +7 -4
- package/src/lib/model/ai-site-build-progress.js.map +1 -1
- package/src/lib/model/ai-site-job.d.ts +19 -2
- package/src/lib/model/ai-site-job.js +29 -8
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/model/ai-site-kinds.d.ts +4 -1
- package/src/lib/model/ai-site-kinds.js +34 -6
- package/src/lib/model/ai-site-kinds.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +37 -1
- package/src/lib/runtime/ai-doctrine-validators.js +151 -6
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +4 -3
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-palette.generated.js +53 -7
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/server/ai-jobs-cancel.d.ts +13 -5
- package/src/lib/server/ai-jobs-cancel.js +27 -9
- package/src/lib/server/ai-jobs-cancel.js.map +1 -1
- package/src/lib/server/ai-jobs-gate.d.ts +12 -2
- package/src/lib/server/ai-jobs-gate.js +20 -9
- package/src/lib/server/ai-jobs-gate.js.map +1 -1
- package/src/lib/usage/ai-usage-meter.d.ts +2 -1
- package/src/lib/usage/ai-usage-meter.js +17 -1
- package/src/lib/usage/ai-usage-meter.js.map +1 -1
- package/src/lib/usage/ai-usage-wire.d.ts +6 -0
- package/src/lib/usage/ai-usage-wire.js.map +1 -1
- package/src/lib/usage/assist-usage.d.ts +7 -0
- package/src/lib/usage/assist-usage.js +14 -4
- package/src/lib/usage/assist-usage.js.map +1 -1
- package/src/lib/usage/free-ai-credits-left.d.ts +7 -0
- package/src/lib/usage/free-ai-credits-left.js +4 -2
- package/src/lib/usage/free-ai-credits-left.js.map +1 -1
package/src/lib/jobs/ai-jobs.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
762
|
-
|
|
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
|
|
892
|
-
*
|
|
893
|
-
*
|
|
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
|
-
*
|
|
897
|
-
*
|
|
898
|
-
*
|
|
899
|
-
*
|
|
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
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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.
|