fix(ai): read AI switches, modules and consent afresh inside background jobs

A background job opens one wide event for its whole run, and the per-request
memo cached the operator switches, module availability, module preferences,
provider presence and consent kinds on it. The nightly status passes loop
over every candidate inside that one event, so a switch turned off mid-run
did not stop the remaining users, and the wire re-check read the same cached
answer.

memoizePerRequest takes a freshInBackground option; every input that decides
whether AI work may run sets it, so a background caller reads them on every
call while a request still reads them once.
This commit is contained in:
Marc-André Bombeck committed 2026-09-24 12:08:39 +02:00
1 parent 4ae231d757
commit 06251d7219
10 files changed
+236 -67

No files matched your search

+26 -22
View File
@@ -169,28 +169,32 @@ export function resolveModuleEnabled(
/** Internal: load the gate inputs once per request. */
function loadInputs(userId: string): Promise<ModuleGateInputs> {
return memoizePerRequest(`module-gate-inputs:${userId}`, async () => {
const [user, cycleProfile] = await Promise.all([
prisma.user.findUnique({
where: { id: userId },
select: {
gender: true,
disableCoach: true,
modulePreferencesJson: true,
},
}),
prisma.cycleProfile.findUnique({
where: { userId },
select: { cycleTrackingEnabled: true },
}),
]);
return {
gender: user?.gender ?? null,
disableCoach: user?.disableCoach ?? false,
modulePreferences: normalisePrefs(user?.modulePreferencesJson),
cycleTrackingEnabled: cycleProfile?.cycleTrackingEnabled ?? null,
};
});
return memoizePerRequest(
`module-gate-inputs:${userId}`,
async () => {
const [user, cycleProfile] = await Promise.all([
prisma.user.findUnique({
where: { id: userId },
select: {
gender: true,
disableCoach: true,
modulePreferencesJson: true,
},
}),
prisma.cycleProfile.findUnique({
where: { userId },
select: { cycleTrackingEnabled: true },
}),
]);
return {
gender: user?.gender ?? null,
disableCoach: user?.disableCoach ?? false,
modulePreferences: normalisePrefs(user?.modulePreferencesJson),
cycleTrackingEnabled: cycleProfile?.cycleTrackingEnabled ?? null,
};
},
{ freshInBackground: true },
);
}
/**
+11 -7
View File
@@ -104,13 +104,17 @@ async function loadAvailabilityBlob(): Promise<unknown> {
* all-available so the modules stay visible on first boot.
*/
export async function getOperatorModuleAvailability(): Promise<OperatorModuleAvailability> {
return memoizePerRequest("operator-module-availability", async () => {
const [blob, switches] = await Promise.all([
loadAvailabilityBlob(),
getAssistantFlags(),
]);
return resolveOperatorAvailability(blob, switches.coach);
});
return memoizePerRequest(
"operator-module-availability",
async () => {
const [blob, switches] = await Promise.all([
loadAvailabilityBlob(),
getAssistantFlags(),
]);
return resolveOperatorAvailability(blob, switches.coach);
},
{ freshInBackground: true },
);
}
/** A module key whose operator layer the blob owns (every key but the Coach). */