feat: scheduled chats are experimental / default-off in v1

Per the v1 scope decision: ship a correct single-process, autonomous core; defer HITL
resume of scheduled runs and clustered operation behind the flag as fast-follows.

getLimits now resolves to disabled unless an admin explicitly opts in via
interface.schedules (true, or an object without use:false). Absence/null/false all mean
off, so a deployment that never opts in never runs the scheduler. initializeScheduleEngine
refuses to arm when the base config leaves it disabled, so "off" means the engine is not
running at all (no tick, no claims) rather than running and refusing at dispatch.

The per-principal availability and the base-only global kill switch are unchanged; this
only flips the default and gates engine startup.
This commit is contained in:
Danny Avila 2026-07-24 15:30:15 -04:00
parent 3a101430f7
commit 52ac901625

View file

@ -204,12 +204,14 @@ export function createSchedulesService(deps: SchedulesServiceDeps): SchedulesSer
? await deps.getAppConfig(getAppConfigOptionsFromUser(user))
: await deps.getAppConfig();
const config = appConfig?.interfaceConfig?.schedules;
// Disabled config is a hard stop: the engine must not keep firing existing
// schedules after an admin turns the feature off.
if (config === false) {
// EXPERIMENTAL, default-OFF (v1): scheduled chats are disabled unless an admin
// explicitly enables them. Absence, null, or `false` all resolve to disabled, so a
// deployment that never opts in never runs the scheduler. `true` uses the defaults;
// an object opts in unless it sets `use: false`.
if (config == null || config === false) {
return { ...DEFAULT_SCHEDULE_LIMITS, enabled: false };
}
if (config == null || typeof config === 'boolean') {
if (config === true) {
return DEFAULT_SCHEDULE_LIMITS;
}
return {
@ -431,6 +433,13 @@ export function createSchedulesService(deps: SchedulesServiceDeps): SchedulesSer
if (engine != null) {
return engine;
}
// EXPERIMENTAL default-off: do not arm the engine unless the base config enables the
// feature. Keeps "off" meaning the scheduler genuinely is not running (no tick loop,
// no claims), rather than running and refusing at the last step.
if (!(await getLimits()).enabled) {
logger.info('[schedules] disabled by config; engine not started (set interface.schedules)');
return undefined;
}
// A clustered deployment (multiple replicas) that is NOT Redis-backed has
// private per-worker job stores, so isJobStoreShared reads false and the
// reconciler skips cross-worker orphan reaping it can't trust.