@aglyn/plugins-ai 1.0.0-beta.266 → 1.0.0-beta.269
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 +18 -18
- package/src/lib/components/ai-job-site.d.ts +8 -1
- package/src/lib/components/ai-job-site.js +7 -0
- package/src/lib/components/ai-job-site.js.map +1 -1
- package/src/lib/components/ai-site-build-page.component.js +37 -0
- package/src/lib/components/ai-site-build-page.component.js.map +1 -1
- package/src/lib/components/ai-site-start-card.component.js +25 -6
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/components/ai-site-start-steps.component.d.ts +8 -2
- package/src/lib/components/ai-site-start-steps.component.js +55 -26
- package/src/lib/components/ai-site-start-steps.component.js.map +1 -1
- package/src/lib/components/ai-site-start-tracking.d.ts +11 -3
- package/src/lib/components/ai-site-start-tracking.js +28 -9
- package/src/lib/components/ai-site-start-tracking.js.map +1 -1
- package/src/lib/components/assist-jobs-drawer.component.js +24 -2
- package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
- package/src/lib/jobs/ai-empty-site-sweep.d.ts +154 -0
- package/src/lib/jobs/ai-empty-site-sweep.js +346 -0
- package/src/lib/jobs/ai-empty-site-sweep.js.map +1 -0
- package/src/lib/jobs/ai-job-page-language.d.ts +7 -1
- package/src/lib/jobs/ai-job-page-language.js +16 -3
- package/src/lib/jobs/ai-job-page-language.js.map +1 -1
- package/src/lib/jobs/ai-job-site-look.d.ts +2 -1
- package/src/lib/jobs/ai-job-site-look.js +6 -2
- package/src/lib/jobs/ai-job-site-look.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.js +11 -6
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs-beat.d.ts +11 -3
- package/src/lib/jobs/ai-jobs-beat.js +23 -6
- package/src/lib/jobs/ai-jobs-beat.js.map +1 -1
- package/src/lib/jobs/ai-jobs.js +46 -7
- package/src/lib/jobs/ai-jobs.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +43 -0
- package/src/lib/jobs/ai-layout-stock-photos.js +735 -22
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/fixtures/ai-stock-photo-agl3923-recordings.d.ts +35 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl3923-recordings.js +379 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-agl3923-recordings.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-brightvale-recordings.d.ts +35 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-brightvale-recordings.js +294 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-brightvale-recordings.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-mexicano-recordings.d.ts +31 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-mexicano-recordings.js +325 -0
- package/src/lib/jobs/fixtures/ai-stock-photo-mexicano-recordings.js.map +1 -0
- package/src/lib/layout-language/ai-layout-design.js +5 -0
- package/src/lib/layout-language/ai-layout-design.js.map +1 -1
- package/src/lib/layout-language/ai-layout-motion.d.ts +60 -7
- package/src/lib/layout-language/ai-layout-motion.js +231 -4
- package/src/lib/layout-language/ai-layout-motion.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 +68 -4
- package/src/lib/layout-language/ai-layout-pictures.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-look.d.ts +10 -1
- package/src/lib/model/ai-site-look.js +14 -3
- package/src/lib/model/ai-site-look.js.map +1 -1
- package/src/lib/model/ai-site-start-handoff.d.ts +49 -0
- package/src/lib/model/ai-site-start-handoff.js +88 -0
- package/src/lib/model/ai-site-start-handoff.js.map +1 -0
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
import { type SweepAiJobsResult } from './ai-jobs';
|
|
18
|
+
import { type AiEmptySiteSweepResult } from './ai-empty-site-sweep';
|
|
18
19
|
/**
|
|
19
20
|
* Resume AI generation jobs on the AI jobs beat (AGL-2904, AGL-3026).
|
|
20
21
|
*
|
|
@@ -69,10 +70,17 @@ export type AiJobsBeatResult = {
|
|
|
69
70
|
held: true;
|
|
70
71
|
} | ({
|
|
71
72
|
held: false;
|
|
73
|
+
emptySites: AiEmptySiteSweepResult | null;
|
|
72
74
|
} & SweepAiJobsResult);
|
|
73
75
|
/**
|
|
74
|
-
* One beat: the switch, then
|
|
75
|
-
*
|
|
76
|
-
*
|
|
76
|
+
* One beat: the switch, then the empty sites a guided start left that are
|
|
77
|
+
* due for removal (AGL-3936), then one sweep under its own lease owner. The
|
|
78
|
+
* owner is fresh per beat and fixed for the whole sweep, so two beats
|
|
79
|
+
* overlapping inside a lease never read each other's lease as their own.
|
|
80
|
+
*
|
|
81
|
+
* The removals come first, a few a beat inside
|
|
82
|
+
* `AI_EMPTY_SITE_SWEEP_BUDGET_MS`, and whatever they took comes off the
|
|
83
|
+
* sweep's budget, so the route's arithmetic inside `maxDuration` holds. A
|
|
84
|
+
* removal that fails never holds up a job's step.
|
|
77
85
|
*/
|
|
78
86
|
export declare function runAiJobsBeat(): Promise<AiJobsBeatResult>;
|
|
@@ -17,6 +17,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
17
17
|
*/ import { randomUUID } from "crypto";
|
|
18
18
|
import { featureLockdownRefusal, firebaseAdmin } from "@aglyn/tenant-data-admin";
|
|
19
19
|
import { AI_JOB_SWEEP_BUDGET_MS, sweepAiJobs } from "./ai-jobs.js";
|
|
20
|
+
import { sweepAiEmptySites } from "./ai-empty-site-sweep.js";
|
|
20
21
|
/**
|
|
21
22
|
* Resume AI generation jobs on the AI jobs beat (AGL-2904, AGL-3026).
|
|
22
23
|
*
|
|
@@ -64,9 +65,15 @@ import { AI_JOB_SWEEP_BUDGET_MS, sweepAiJobs } from "./ai-jobs.js";
|
|
|
64
65
|
* with the sweep's function time, so the plugin dispatcher never serves it.
|
|
65
66
|
*/ export const AI_JOBS_BEAT_PATH = 'admin/ai-jobs-beat';
|
|
66
67
|
/**
|
|
67
|
-
* One beat: the switch, then
|
|
68
|
-
*
|
|
69
|
-
*
|
|
68
|
+
* One beat: the switch, then the empty sites a guided start left that are
|
|
69
|
+
* due for removal (AGL-3936), then one sweep under its own lease owner. The
|
|
70
|
+
* owner is fresh per beat and fixed for the whole sweep, so two beats
|
|
71
|
+
* overlapping inside a lease never read each other's lease as their own.
|
|
72
|
+
*
|
|
73
|
+
* The removals come first, a few a beat inside
|
|
74
|
+
* `AI_EMPTY_SITE_SWEEP_BUDGET_MS`, and whatever they took comes off the
|
|
75
|
+
* sweep's budget, so the route's arithmetic inside `maxDuration` holds. A
|
|
76
|
+
* removal that fails never holds up a job's step.
|
|
70
77
|
*/ export async function runAiJobsBeat() {
|
|
71
78
|
const locked = await featureLockdownRefusal({
|
|
72
79
|
feature: 'ai-generate'
|
|
@@ -77,16 +84,26 @@ import { AI_JOB_SWEEP_BUDGET_MS, sweepAiJobs } from "./ai-jobs.js";
|
|
|
77
84
|
held: true
|
|
78
85
|
};
|
|
79
86
|
}
|
|
87
|
+
const firestore = firebaseAdmin.app().firestore();
|
|
88
|
+
const startedAt = Date.now();
|
|
89
|
+
const emptySites = await sweepAiEmptySites({
|
|
90
|
+
firestore
|
|
91
|
+
}).catch((error)=>{
|
|
92
|
+
console.error('ai jobs beat: the empty site sweep failed', error);
|
|
93
|
+
return null;
|
|
94
|
+
});
|
|
95
|
+
if (emptySites == null ? void 0 : emptySites.removed) console.info(`ai jobs: ${emptySites.removed} empty site(s) removed`);
|
|
80
96
|
const result = await sweepAiJobs({
|
|
81
|
-
firestore
|
|
97
|
+
firestore,
|
|
82
98
|
owner: `beat:${randomUUID()}`,
|
|
83
|
-
budgetMs: AI_JOB_SWEEP_BUDGET_MS
|
|
99
|
+
budgetMs: Math.max(0, AI_JOB_SWEEP_BUDGET_MS - (Date.now() - startedAt))
|
|
84
100
|
});
|
|
85
101
|
if (result.due) {
|
|
86
102
|
console.info(`ai jobs: ${result.ran} step(s) run, ${result.skipped} skipped, ` + `${result.paused} held by a workspace's AI pause, ` + `${result.remaining} left for the next beat` + (result.budgetExhausted ? ' (budget exhausted)' : ''));
|
|
87
103
|
}
|
|
88
104
|
return _extends({
|
|
89
|
-
held: false
|
|
105
|
+
held: false,
|
|
106
|
+
emptySites
|
|
90
107
|
}, result);
|
|
91
108
|
}
|
|
92
109
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-jobs-beat.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 { randomUUID } from 'crypto'\nimport { featureLockdownRefusal, firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { AI_JOB_SWEEP_BUDGET_MS, sweepAiJobs, type SweepAiJobsResult } from './ai-jobs'\n\n/**\n * Resume AI generation jobs on the AI jobs beat (AGL-2904, AGL-3026).\n *\n * A job's first step runs inline in the console route when it can; every\n * step after it, every step too long for an inline door, and any step a\n * budget cut short, runs here. The sweep bounds itself at\n * `AI_JOB_SWEEP_BUDGET_MS` of wall clock and hands the rest to the next beat\n * — the ordering of the queue is the cursor, so nothing is stored between\n * beats and nothing can be skipped.\n *\n * ## Why the console runs it\n *\n * A step calls the AI provider, and the provider's key is held by the\n * console alone: the tenant app serves every published site, and a\n * credential it holds is one request-handling bug away from a visitor. So\n * the beat is a console route (`server/ai-jobs-beat-route.ts`, registered\n * by the console surface), driven every minute by its own Cloud Scheduler\n * job in `cloud/functions` on the console's cron secret — the reason the\n * sending-domain sweep is a console route and not a job on the tenant's\n * platform beat. A beat must resolve to the minute: a step handed back by\n * a budget, or a page pass waiting on the next, should resume on the next\n * minute, not the next quarter hour.\n *\n * ## The kill switch\n *\n * `ai-generate` on the staff lockdown page is the lever for a provider\n * incident or a spend runaway on the generative doors, and this beat is the\n * one generative caller with no request to refuse. So it asks the same\n * feature verdict a door would — the platform scope and then the feature\n * document — and does nothing while either is locked. A job it leaves\n * queued stays queued: its lease is not taken, so nothing expires and\n * nothing is lost, and the first beat after the lock lifts picks it up.\n *\n * The same switch paused for ONE workspace (AGL-3037) holds that workspace's\n * jobs and no other's: `runAiJobStep` asks the verdict with the job's org\n * before it claims a step, and leaves a paused workspace's job queued.\n */\n\n/** The beat's row in `SCHEDULED_JOBS`, and the mark it leaves for `/api/health/crons`. */\nexport const AI_JOBS_BEAT_CRON_ID = 'ai-jobs-beat'\n\n/**\n * The console path the beat is registered at: the one path this plugin\n * registers outside its own prefixes. `/api/admin/` is where the console's\n * scheduled sweeps live, and the console's edge lets a request past its bot\n * challenge there when it carries the cron secret's header — a route\n * anywhere else would need a firewall rule of its own before a scheduler\n * could reach it. The console app serves this exact path from a named route\n * with the sweep's function time, so the plugin dispatcher never serves it.\n */\nexport const AI_JOBS_BEAT_PATH = 'admin/ai-jobs-beat'\n\n/** What one beat did: nothing while `ai-generate` is locked, else the sweep's account. */\nexport type AiJobsBeatResult =\n | { held: true }\n | ({ held: false } & SweepAiJobsResult)\n\n/**\n * One beat: the switch, then one sweep under its own lease owner. The
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-jobs-beat.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 { randomUUID } from 'crypto'\nimport { featureLockdownRefusal, firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { AI_JOB_SWEEP_BUDGET_MS, sweepAiJobs, type SweepAiJobsResult } from './ai-jobs'\nimport { sweepAiEmptySites, type AiEmptySiteSweepResult } from './ai-empty-site-sweep'\n\n/**\n * Resume AI generation jobs on the AI jobs beat (AGL-2904, AGL-3026).\n *\n * A job's first step runs inline in the console route when it can; every\n * step after it, every step too long for an inline door, and any step a\n * budget cut short, runs here. The sweep bounds itself at\n * `AI_JOB_SWEEP_BUDGET_MS` of wall clock and hands the rest to the next beat\n * — the ordering of the queue is the cursor, so nothing is stored between\n * beats and nothing can be skipped.\n *\n * ## Why the console runs it\n *\n * A step calls the AI provider, and the provider's key is held by the\n * console alone: the tenant app serves every published site, and a\n * credential it holds is one request-handling bug away from a visitor. So\n * the beat is a console route (`server/ai-jobs-beat-route.ts`, registered\n * by the console surface), driven every minute by its own Cloud Scheduler\n * job in `cloud/functions` on the console's cron secret — the reason the\n * sending-domain sweep is a console route and not a job on the tenant's\n * platform beat. A beat must resolve to the minute: a step handed back by\n * a budget, or a page pass waiting on the next, should resume on the next\n * minute, not the next quarter hour.\n *\n * ## The kill switch\n *\n * `ai-generate` on the staff lockdown page is the lever for a provider\n * incident or a spend runaway on the generative doors, and this beat is the\n * one generative caller with no request to refuse. So it asks the same\n * feature verdict a door would — the platform scope and then the feature\n * document — and does nothing while either is locked. A job it leaves\n * queued stays queued: its lease is not taken, so nothing expires and\n * nothing is lost, and the first beat after the lock lifts picks it up.\n *\n * The same switch paused for ONE workspace (AGL-3037) holds that workspace's\n * jobs and no other's: `runAiJobStep` asks the verdict with the job's org\n * before it claims a step, and leaves a paused workspace's job queued.\n */\n\n/** The beat's row in `SCHEDULED_JOBS`, and the mark it leaves for `/api/health/crons`. */\nexport const AI_JOBS_BEAT_CRON_ID = 'ai-jobs-beat'\n\n/**\n * The console path the beat is registered at: the one path this plugin\n * registers outside its own prefixes. `/api/admin/` is where the console's\n * scheduled sweeps live, and the console's edge lets a request past its bot\n * challenge there when it carries the cron secret's header — a route\n * anywhere else would need a firewall rule of its own before a scheduler\n * could reach it. The console app serves this exact path from a named route\n * with the sweep's function time, so the plugin dispatcher never serves it.\n */\nexport const AI_JOBS_BEAT_PATH = 'admin/ai-jobs-beat'\n\n/** What one beat did: nothing while `ai-generate` is locked, else the sweep's account. */\nexport type AiJobsBeatResult =\n | { held: true }\n | ({ held: false; emptySites: AiEmptySiteSweepResult | null } & SweepAiJobsResult)\n\n/**\n * One beat: the switch, then the empty sites a guided start left that are\n * due for removal (AGL-3936), then one sweep under its own lease owner. The\n * owner is fresh per beat and fixed for the whole sweep, so two beats\n * overlapping inside a lease never read each other's lease as their own.\n *\n * The removals come first, a few a beat inside\n * `AI_EMPTY_SITE_SWEEP_BUDGET_MS`, and whatever they took comes off the\n * sweep's budget, so the route's arithmetic inside `maxDuration` holds. A\n * removal that fails never holds up a job's step.\n */\nexport async function runAiJobsBeat(): Promise<AiJobsBeatResult> {\n const locked = await featureLockdownRefusal({ feature: 'ai-generate' })\n if (locked) {\n console.warn('ai jobs beat held: ai-generate is locked')\n return { held: true }\n }\n const firestore = firebaseAdmin.app().firestore()\n const startedAt = Date.now()\n const emptySites = await sweepAiEmptySites({ firestore }).catch((error: unknown) => {\n console.error('ai jobs beat: the empty site sweep failed', error)\n return null\n })\n if (emptySites?.removed) console.info(`ai jobs: ${emptySites.removed} empty site(s) removed`)\n const result = await sweepAiJobs({\n firestore,\n owner: `beat:${randomUUID()}`,\n budgetMs: Math.max(0, AI_JOB_SWEEP_BUDGET_MS - (Date.now() - startedAt)),\n })\n if (result.due) {\n console.info(\n `ai jobs: ${result.ran} step(s) run, ${result.skipped} skipped, ` +\n `${result.paused} held by a workspace's AI pause, ` +\n `${result.remaining} left for the next beat` +\n (result.budgetExhausted ? ' (budget exhausted)' : ''),\n )\n }\n return { held: false, emptySites, ...result }\n}\n"],"names":["randomUUID","featureLockdownRefusal","firebaseAdmin","AI_JOB_SWEEP_BUDGET_MS","sweepAiJobs","sweepAiEmptySites","AI_JOBS_BEAT_CRON_ID","AI_JOBS_BEAT_PATH","runAiJobsBeat","locked","feature","console","warn","held","firestore","app","startedAt","Date","now","emptySites","catch","error","removed","info","result","owner","budgetMs","Math","max","due","ran","skipped","paused","remaining","budgetExhausted"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,UAAU,QAAQ,SAAQ;AACnC,SAASC,sBAAsB,EAAEC,aAAa,QAAQ,2BAA0B;AAChF,SAASC,sBAAsB,EAAEC,WAAW,QAAgC,eAAW;AACvF,SAASC,iBAAiB,QAAqC,2BAAuB;AAEtF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoCC,GAED,wFAAwF,GACxF,OAAO,MAAMC,uBAAuB,eAAc;AAElD;;;;;;;;CAQC,GACD,OAAO,MAAMC,oBAAoB,qBAAoB;AAOrD;;;;;;;;;;CAUC,GACD,OAAO,eAAeC;IACpB,MAAMC,SAAS,MAAMR,uBAAuB;QAAES,SAAS;IAAc;IACrE,IAAID,QAAQ;QACVE,QAAQC,IAAI,CAAC;QACb,OAAO;YAAEC,MAAM;QAAK;IACtB;IACA,MAAMC,YAAYZ,cAAca,GAAG,GAAGD,SAAS;IAC/C,MAAME,YAAYC,KAAKC,GAAG;IAC1B,MAAMC,aAAa,MAAMd,kBAAkB;QAAES;IAAU,GAAGM,KAAK,CAAC,CAACC;QAC/DV,QAAQU,KAAK,CAAC,6CAA6CA;QAC3D,OAAO;IACT;IACA,IAAIF,8BAAAA,WAAYG,OAAO,EAAEX,QAAQY,IAAI,CAAC,CAAC,SAAS,EAAEJ,WAAWG,OAAO,CAAC,sBAAsB,CAAC;IAC5F,MAAME,SAAS,MAAMpB,YAAY;QAC/BU;QACAW,OAAO,CAAC,KAAK,EAAEzB,cAAc;QAC7B0B,UAAUC,KAAKC,GAAG,CAAC,GAAGzB,yBAA0Bc,CAAAA,KAAKC,GAAG,KAAKF,SAAQ;IACvE;IACA,IAAIQ,OAAOK,GAAG,EAAE;QACdlB,QAAQY,IAAI,CACV,CAAC,SAAS,EAAEC,OAAOM,GAAG,CAAC,cAAc,EAAEN,OAAOO,OAAO,CAAC,UAAU,CAAC,GAC/D,GAAGP,OAAOQ,MAAM,CAAC,iCAAiC,CAAC,GACnD,GAAGR,OAAOS,SAAS,CAAC,uBAAuB,CAAC,GAC3CT,CAAAA,OAAOU,eAAe,GAAG,wBAAwB,EAAC;IAEzD;IACA,OAAO;QAAErB,MAAM;QAAOM;OAAeK;AACvC"}
|
package/src/lib/jobs/ai-jobs.js
CHANGED
|
@@ -47,6 +47,7 @@ import { AI_SITE_GUIDED_BUILD_FAILED_COPY, aiCustomerSafeCopy, aiCustomerSafeIte
|
|
|
47
47
|
import { aiSiteStartInputsOf } from "../model/ai-site-start.js";
|
|
48
48
|
import { addAdminAudit } from "@aglyn/tenant-data-admin/server/admin-audit-write";
|
|
49
49
|
import { AI_JOB_CANCELED_ITEM_NOTE, aiJobCancelPending, isAiJobCanceledError, watchAiJobCancel } from "./ai-job-cancel.js";
|
|
50
|
+
import { clearAiEmptySiteSweep, scheduleAiEmptySiteSweep } from "./ai-empty-site-sweep.js";
|
|
50
51
|
/**
|
|
51
52
|
* AI generation jobs — the Firestore state machine (AGL-2904).
|
|
52
53
|
*
|
|
@@ -256,6 +257,16 @@ let transitionListener = null;
|
|
|
256
257
|
/** Whether a transition listener is registered: what a surface that runs jobs must have. */ export function aiJobTransitionListenerRegistered() {
|
|
257
258
|
return transitionListener !== null;
|
|
258
259
|
}
|
|
260
|
+
/**
|
|
261
|
+
* A job just ENDED `canceled` or `failed`, in the write the caller made
|
|
262
|
+
* (AGL-3936): a site start that built nothing schedules the removal of the
|
|
263
|
+
* empty site it was for, an hour out (`ai-empty-site-sweep.ts`). Every write
|
|
264
|
+
* that ends a job calls this once, after its transaction commits. Never
|
|
265
|
+
* throws.
|
|
266
|
+
*/ async function noteAiJobEnded(firestore, job, now) {
|
|
267
|
+
if (!job || job.status !== 'canceled' && job.status !== 'failed') return;
|
|
268
|
+
await scheduleAiEmptySiteSweep(firestore, job, now);
|
|
269
|
+
}
|
|
259
270
|
/** Tells the listener; a listener that throws never fails the write it follows. */ async function announceAiJobTransition(job, to, failure) {
|
|
260
271
|
if (!transitionListener) return;
|
|
261
272
|
try {
|
|
@@ -408,6 +419,8 @@ function jobFrom(snapshot) {
|
|
|
408
419
|
creditsConfirmed: input.creditsConfirmed
|
|
409
420
|
} : {});
|
|
410
421
|
await ref.set(data);
|
|
422
|
+
// A new job on the site — a guided start begun again — keeps it (AGL-3936).
|
|
423
|
+
await clearAiEmptySiteSweep(firestore, input.hostId);
|
|
411
424
|
await logAiJobCreated(input.orgId, {
|
|
412
425
|
uid: input.createdBy,
|
|
413
426
|
email: (_input_createdByEmail = input.createdByEmail) != null ? _input_createdByEmail : null
|
|
@@ -527,6 +540,8 @@ export async function getAiJob(firestore, orgId, jobId) {
|
|
|
527
540
|
publishAsked: true
|
|
528
541
|
} : {}, aiJobCancelPending(job) ? {
|
|
529
542
|
cancelRequested: true
|
|
543
|
+
} : {}, job.siteRemoved ? {
|
|
544
|
+
siteRemoved: true
|
|
530
545
|
} : {});
|
|
531
546
|
}
|
|
532
547
|
function leaseIsLive(job, owner, nowMs) {
|
|
@@ -607,16 +622,20 @@ function nextStepIndex(job) {
|
|
|
607
622
|
* stale lease is released by being taken, not by a sweep of its own.
|
|
608
623
|
*/ export async function claimNextStep(firestore, orgId, jobId, owner, now = new Date()) {
|
|
609
624
|
const ref = jobsCollection(firestore, orgId).doc(jobId);
|
|
610
|
-
|
|
625
|
+
let ended = null;
|
|
626
|
+
const claimed = await firestore.runTransaction(async (tx)=>{
|
|
627
|
+
ended = null;
|
|
611
628
|
const job = jobFrom(await tx.get(ref));
|
|
612
629
|
if (!job) return null;
|
|
613
630
|
// A cancel asked while a step ran, whose runner never came back to end
|
|
614
631
|
// it (AGL-3616): the lease ran out, so the job ends here, unclaimed.
|
|
615
632
|
if (aiJobCancelPending(job)) {
|
|
616
633
|
if (!leaseIsLive(job, owner, now.getTime())) {
|
|
617
|
-
|
|
634
|
+
const patch = aiJobCanceledPatch(job, now, cancelRequestOf(job, now));
|
|
635
|
+
tx.set(ref, patch, {
|
|
618
636
|
merge: true
|
|
619
637
|
});
|
|
638
|
+
ended = _extends({}, job, patch);
|
|
620
639
|
}
|
|
621
640
|
return null;
|
|
622
641
|
}
|
|
@@ -652,6 +671,8 @@ function nextStepIndex(job) {
|
|
|
652
671
|
stepIndex
|
|
653
672
|
};
|
|
654
673
|
});
|
|
674
|
+
await noteAiJobEnded(firestore, ended, now);
|
|
675
|
+
return claimed;
|
|
655
676
|
}
|
|
656
677
|
/**
|
|
657
678
|
* Extend the lease while a long step is still running. `false` when the
|
|
@@ -689,15 +710,19 @@ function nextStepIndex(job) {
|
|
|
689
710
|
* parked, no step left, or another owner's live lease.
|
|
690
711
|
*/ export async function holdPausedAiJob(firestore, orgId, jobId, owner, now = new Date()) {
|
|
691
712
|
const ref = jobsCollection(firestore, orgId).doc(jobId);
|
|
692
|
-
|
|
713
|
+
let ended = null;
|
|
714
|
+
const held = await firestore.runTransaction(async (tx)=>{
|
|
715
|
+
ended = null;
|
|
693
716
|
const job = jobFrom(await tx.get(ref));
|
|
694
717
|
if (!job) return null;
|
|
695
718
|
// A paused workspace's job a person canceled ends canceled (AGL-3616).
|
|
696
719
|
if (aiJobCancelPending(job)) {
|
|
697
720
|
if (!leaseIsLive(job, owner, now.getTime())) {
|
|
698
|
-
|
|
721
|
+
const patch = aiJobCanceledPatch(job, now, cancelRequestOf(job, now));
|
|
722
|
+
tx.set(ref, patch, {
|
|
699
723
|
merge: true
|
|
700
724
|
});
|
|
725
|
+
ended = _extends({}, job, patch);
|
|
701
726
|
}
|
|
702
727
|
return null;
|
|
703
728
|
}
|
|
@@ -711,6 +736,8 @@ function nextStepIndex(job) {
|
|
|
711
736
|
updatedAt: now
|
|
712
737
|
});
|
|
713
738
|
});
|
|
739
|
+
await noteAiJobEnded(firestore, ended, now);
|
|
740
|
+
return held;
|
|
714
741
|
}
|
|
715
742
|
/**
|
|
716
743
|
* A build's ledger with one pass applied (AGL-3616): the pass's credits on
|
|
@@ -874,9 +901,12 @@ function nextStepIndex(job) {
|
|
|
874
901
|
job: _extends({}, job, patch),
|
|
875
902
|
remaining,
|
|
876
903
|
// Entered `needs_review` in this write, rather than parked again.
|
|
877
|
-
parked: parksForReview && job.status !== 'needs_review'
|
|
904
|
+
parked: parksForReview && job.status !== 'needs_review',
|
|
905
|
+
// Ended `canceled` in this write (AGL-3936).
|
|
906
|
+
ended: canceling && !isAiJobTerminal(job.status)
|
|
878
907
|
};
|
|
879
908
|
});
|
|
909
|
+
if (recorded.ended) await noteAiJobEnded(firestore, recorded.job, now);
|
|
880
910
|
if (recorded.parked && !input.quiet) await announceAiJobTransition(recorded.job, 'needs-review');
|
|
881
911
|
return {
|
|
882
912
|
job: recorded.job,
|
|
@@ -912,6 +942,7 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
912
942
|
updatedAt: now
|
|
913
943
|
});
|
|
914
944
|
if (changed && job.status === 'done') await announceAiJobTransition(job, 'done');
|
|
945
|
+
if (changed) await noteAiJobEnded(firestore, job, now);
|
|
915
946
|
return job;
|
|
916
947
|
}
|
|
917
948
|
/**
|
|
@@ -946,6 +977,7 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
946
977
|
error: (detail == null ? void 0 : detail.error) instanceof Error ? detail.error.message : (detail == null ? void 0 : detail.error) == null ? null : String(detail.error)
|
|
947
978
|
});
|
|
948
979
|
}
|
|
980
|
+
if (changed) await noteAiJobEnded(firestore, job, now);
|
|
949
981
|
return job;
|
|
950
982
|
}
|
|
951
983
|
/**
|
|
@@ -953,7 +985,7 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
953
985
|
* step goes back to `pending` so the same step is what runs when the beat
|
|
954
986
|
* tries again; its attempt is not counted, because nothing was attempted.
|
|
955
987
|
*/ export async function markAiJobNeedsInput(firestore, orgId, jobId, reason, now = new Date()) {
|
|
956
|
-
const { job } = await transition(firestore, orgId, jobId, (current)=>isAiJobTerminal(current.status) ? null : aiJobCancelPending(current) ? aiJobCanceledPatch(current, now, cancelRequestOf(current, now)) : {
|
|
988
|
+
const { job, changed } = await transition(firestore, orgId, jobId, (current)=>isAiJobTerminal(current.status) ? null : aiJobCancelPending(current) ? aiJobCanceledPatch(current, now, cancelRequestOf(current, now)) : {
|
|
957
989
|
status: 'needs_input',
|
|
958
990
|
error: reason,
|
|
959
991
|
lease: null,
|
|
@@ -966,6 +998,7 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
966
998
|
}),
|
|
967
999
|
updatedAt: now
|
|
968
1000
|
});
|
|
1001
|
+
if (changed) await noteAiJobEnded(firestore, job, now);
|
|
969
1002
|
return job;
|
|
970
1003
|
}
|
|
971
1004
|
/**
|
|
@@ -1016,6 +1049,7 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
1016
1049
|
kind: result.job.kind
|
|
1017
1050
|
});
|
|
1018
1051
|
}
|
|
1052
|
+
if (result.changed) await noteAiJobEnded(firestore, result.job, now);
|
|
1019
1053
|
return result;
|
|
1020
1054
|
}
|
|
1021
1055
|
/**
|
|
@@ -1372,6 +1406,8 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
1372
1406
|
updatedAt: now
|
|
1373
1407
|
});
|
|
1374
1408
|
});
|
|
1409
|
+
// A site start tried again keeps its site (AGL-3936).
|
|
1410
|
+
if (result.changed) await clearAiEmptySiteSweep(firestore, result.job.hostId);
|
|
1375
1411
|
return _extends({}, result, {
|
|
1376
1412
|
retried: result.changed ? retried : []
|
|
1377
1413
|
});
|
|
@@ -1388,7 +1424,7 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
1388
1424
|
* and a confirmed plan is held at what the whole job is estimated to cost
|
|
1389
1425
|
* (AGL-3031), creations included, where that is more.
|
|
1390
1426
|
*/ export async function resumeAiJob(firestore, orgId, jobId, actor, now = new Date(), options = {}) {
|
|
1391
|
-
|
|
1427
|
+
const resumed = await transition(firestore, orgId, jobId, (current)=>{
|
|
1392
1428
|
var _options_reducePlan;
|
|
1393
1429
|
var _current_review, _current_inputs;
|
|
1394
1430
|
// A job the meter paused (AGL-3660) carries on from the step it paused
|
|
@@ -1435,6 +1471,9 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
1435
1471
|
updatedAt: now
|
|
1436
1472
|
});
|
|
1437
1473
|
});
|
|
1474
|
+
// A site start resumed keeps its site (AGL-3936).
|
|
1475
|
+
if (resumed.changed) await clearAiEmptySiteSweep(firestore, resumed.job.hostId);
|
|
1476
|
+
return resumed;
|
|
1438
1477
|
}
|
|
1439
1478
|
/**
|
|
1440
1479
|
* Record what a person applied from a finished job's outputs (AGL-2910): the
|