{"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../src/scheduler.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type { Dirent } from \"node:fs\";\nimport { readdir, readFile, stat } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { type EvoPaths, ensureEvoLayout } from \"./paths.ts\";\nimport { BundleRegistry } from \"./registry/registry.ts\";\nimport {\n\tappendJsonLine,\n\tatomicWriteJson,\n\treadJsonIfExists,\n\ttruncateIncompleteFinalLine,\n\ttryWithFileLock,\n\twithFileLock,\n} from \"./storage.ts\";\n\nconst SCHEDULE_SCHEMA_VERSION = 1;\nconst SCHEDULE_CONFIG_SCHEMA_VERSION = 1;\nconst MINUTE_MS = 60_000;\nconst HOUR_MS = 60 * MINUTE_MS;\nconst DAY_MS = 24 * HOUR_MS;\nconst MAX_SCHEDULE_DAYS = 365;\nconst PAUSE_POLL_MS = 50;\n\nexport interface QuietHours {\n\tstart: string;\n\tend: string;\n}\n\nexport interface ScheduledImproveConfig {\n\tquietHours: QuietHours | null;\n\tinactivityMinutes: number;\n\tdailyRunLimit: number;\n\teveryDays: number;\n\tlockStaleMs: number;\n}\n\nexport interface ScheduledImproveConfigInput {\n\tquietHours?: Partial<QuietHours> | null;\n\tinactivityMinutes?: number;\n\tdailyRunLimit?: number;\n\teveryDays?: number;\n\tlockStaleMs?: number;\n}\n\nexport const DEFAULT_SCHEDULED_IMPROVE_CONFIG: Readonly<ScheduledImproveConfig> = {\n\tquietHours: { start: \"00:00\", end: \"06:00\" },\n\tinactivityMinutes: 15,\n\tdailyRunLimit: 1,\n\teveryDays: 1,\n\tlockStaleMs: 12 * HOUR_MS,\n};\n\nexport type EvoScheduleMode = \"auto\" | \"manual\";\n\nexport interface EvoScheduleConfig {\n\tschemaVersion: 1;\n\tmode: EvoScheduleMode;\n\teveryDays: number;\n\tquietHours: QuietHours | null;\n\tinactivityMinutes: number;\n\tdailyRunLimit: number;\n\ttrialDueAfterDays: number;\n\ttrialDueAfterSessions: number;\n}\n\nexport interface EvoScheduleConfigInput {\n\tmode?: EvoScheduleMode;\n\teveryDays?: number;\n\tquietHours?: QuietHours | null;\n\tinactivityMinutes?: number;\n\tdailyRunLimit?: number;\n\ttrialDueAfterDays?: number;\n\ttrialDueAfterSessions?: number;\n}\n\nexport const DEFAULT_EVO_SCHEDULE_CONFIG: Readonly<EvoScheduleConfig> = {\n\tschemaVersion: SCHEDULE_CONFIG_SCHEMA_VERSION,\n\tmode: \"auto\",\n\teveryDays: 3,\n\tquietHours: null,\n\tinactivityMinutes: 15,\n\tdailyRunLimit: 1,\n\ttrialDueAfterDays: 7,\n\ttrialDueAfterSessions: 10,\n};\n\nexport interface ImproveRunStartedEvent {\n\tschemaVersion: 1;\n\ttype: \"started\";\n\trunId: string;\n\ttimestamp: string;\n\tlocalDay: string;\n}\n\nexport interface ImproveRunCompletedEvent {\n\tschemaVersion: 1;\n\ttype: \"completed\";\n\trunId: string;\n\ttimestamp: string;\n\tlocalDay: string;\n}\n\nexport interface ImproveRunFailedEvent {\n\tschemaVersion: 1;\n\ttype: \"failed\";\n\trunId: string;\n\ttimestamp: string;\n\tlocalDay: string;\n\terror: string;\n}\n\nexport type ImproveRunEvent = ImproveRunStartedEvent | ImproveRunCompletedEvent | ImproveRunFailedEvent;\n\nexport type ScheduledImproveSkipReason =\n\t| \"already-running\"\n\t| \"paused\"\n\t| \"manual-mode\"\n\t| \"outside-quiet-hours\"\n\t| \"interval-not-elapsed\"\n\t| \"recent-session-activity\"\n\t| \"daily-limit-reached\";\n\nexport type ScheduledImproveResult<T> =\n\t| {\n\t\t\tstatus: \"completed\";\n\t\t\trunId: string;\n\t\t\tvalue: T;\n\t  }\n\t| {\n\t\t\tstatus: \"skipped\";\n\t\t\treason: ScheduledImproveSkipReason;\n\t\t\tlastActivityAt?: string;\n\t\t\tnextEligibleDay?: string;\n\t  };\n\nexport interface RunScheduledImproveOptions<T> {\n\tpaths: EvoPaths;\n\timprove: (signal: AbortSignal) => Promise<T>;\n\tconfig?: ScheduledImproveConfigInput;\n\tnow?: () => Date;\n\texcludeSessionIds?: readonly string[];\n\tcreateOwnerToken?: () => string;\n\tafterLockPrepared?: () => void | Promise<void>;\n}\n\nfunction getErrorCode(error: unknown): string | undefined {\n\tif (typeof error !== \"object\" || error === null || !(\"code\" in error)) return undefined;\n\treturn typeof error.code === \"string\" ? error.code : undefined;\n}\n\nfunction errorMessage(error: unknown): string {\n\treturn error instanceof Error ? error.message : String(error);\n}\n\nfunction parseClock(value: string, label: string): number {\n\tconst match = /^(\\d{2}):(\\d{2})$/.exec(value);\n\tif (!match) throw new Error(`${label} must use HH:MM format`);\n\tconst hour = Number(match[1]);\n\tconst minute = Number(match[2]);\n\tif (hour > 23 || minute > 59) throw new Error(`${label} is outside the valid clock range`);\n\treturn hour * 60 + minute;\n}\n\nfunction requirePositiveSafeInteger(value: number, label: string): number {\n\tif (!Number.isSafeInteger(value) || value <= 0) throw new Error(`${label} must be a positive safe integer`);\n\treturn value;\n}\n\nfunction resolveQuietHours(input: Partial<QuietHours> | null | undefined): QuietHours | null {\n\tif (input === null) return null;\n\tconst defaults = DEFAULT_SCHEDULED_IMPROVE_CONFIG.quietHours ?? { start: \"00:00\", end: \"06:00\" };\n\tconst quietHours = {\n\t\tstart: input?.start ?? defaults.start,\n\t\tend: input?.end ?? defaults.end,\n\t};\n\tconst start = parseClock(quietHours.start, \"quietHours.start\");\n\tconst end = parseClock(quietHours.end, \"quietHours.end\");\n\tif (start === end) throw new Error(\"quietHours start and end must differ\");\n\treturn quietHours;\n}\n\nfunction requireScheduleDays(value: number, label: string): number {\n\trequirePositiveSafeInteger(value, label);\n\tif (value > MAX_SCHEDULE_DAYS) throw new Error(`${label} must be at most ${MAX_SCHEDULE_DAYS} days`);\n\treturn value;\n}\n\nfunction resolveScheduledImproveConfig(input: ScheduledImproveConfigInput = {}): ScheduledImproveConfig {\n\treturn {\n\t\tquietHours: resolveQuietHours(\n\t\t\tinput.quietHours === undefined ? DEFAULT_SCHEDULED_IMPROVE_CONFIG.quietHours : input.quietHours,\n\t\t),\n\t\tinactivityMinutes: requirePositiveSafeInteger(\n\t\t\tinput.inactivityMinutes ?? DEFAULT_SCHEDULED_IMPROVE_CONFIG.inactivityMinutes,\n\t\t\t\"inactivityMinutes\",\n\t\t),\n\t\tdailyRunLimit: requirePositiveSafeInteger(\n\t\t\tinput.dailyRunLimit ?? DEFAULT_SCHEDULED_IMPROVE_CONFIG.dailyRunLimit,\n\t\t\t\"dailyRunLimit\",\n\t\t),\n\t\teveryDays: requireScheduleDays(input.everyDays ?? DEFAULT_SCHEDULED_IMPROVE_CONFIG.everyDays, \"everyDays\"),\n\t\tlockStaleMs: requirePositiveSafeInteger(\n\t\t\tinput.lockStaleMs ?? DEFAULT_SCHEDULED_IMPROVE_CONFIG.lockStaleMs,\n\t\t\t\"lockStaleMs\",\n\t\t),\n\t};\n}\n\nexport function isWithinQuietHours(date: Date, quietHours: QuietHours): boolean {\n\tconst start = parseClock(quietHours.start, \"quietHours.start\");\n\tconst end = parseClock(quietHours.end, \"quietHours.end\");\n\tif (start === end) throw new Error(\"quietHours start and end must differ\");\n\tconst minute = date.getHours() * 60 + date.getMinutes();\n\treturn start < end ? minute >= start && minute < end : minute >= start || minute < end;\n}\n\nexport function localDay(date: Date): string {\n\tconst year = String(date.getFullYear()).padStart(4, \"0\");\n\tconst month = String(date.getMonth() + 1).padStart(2, \"0\");\n\tconst day = String(date.getDate()).padStart(2, \"0\");\n\treturn `${year}-${month}-${day}`;\n}\n\nfunction parseLocalDayMs(day: string): number {\n\tconst match = /^(\\d{4})-(\\d{2})-(\\d{2})$/.exec(day);\n\tif (!match) throw new Error(\"localDay must use YYYY-MM-DD format\");\n\treturn Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]));\n}\n\nexport function addLocalDays(day: string, days: number): string {\n\treturn new Date(parseLocalDayMs(day) + days * DAY_MS).toISOString().slice(0, 10);\n}\n\nexport function latestCompletedImproveDay(events: readonly ImproveRunEvent[]): string | undefined {\n\tlet latest: string | undefined;\n\tfor (const event of events) {\n\t\tif (event.type === \"completed\" && (!latest || event.localDay > latest)) latest = event.localDay;\n\t}\n\treturn latest;\n}\n\nexport function getImproveRunsPath(paths: EvoPaths): string {\n\treturn join(paths.registry, \"improve-runs.jsonl\");\n}\n\nexport function getImproveLockPath(paths: EvoPaths): string {\n\treturn join(paths.locks, \"scheduled-improve.lock\");\n}\n\nfunction parseImproveRunEvent(value: unknown, lineNumber: number): ImproveRunEvent {\n\tif (typeof value !== \"object\" || value === null) {\n\t\tthrow new Error(`Invalid improve run event at line ${lineNumber}`);\n\t}\n\tconst event = value as Record<string, unknown>;\n\tif (\n\t\tevent.schemaVersion !== SCHEDULE_SCHEMA_VERSION ||\n\t\t!(event.type === \"started\" || event.type === \"completed\" || event.type === \"failed\") ||\n\t\ttypeof event.runId !== \"string\" ||\n\t\t!event.runId ||\n\t\ttypeof event.timestamp !== \"string\" ||\n\t\t!Number.isFinite(Date.parse(event.timestamp)) ||\n\t\ttypeof event.localDay !== \"string\" ||\n\t\t!/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/.test(event.localDay) ||\n\t\t(event.type === \"failed\" && typeof event.error !== \"string\")\n\t) {\n\t\tthrow new Error(`Invalid improve run event at line ${lineNumber}`);\n\t}\n\treturn event as unknown as ImproveRunEvent;\n}\n\nexport async function readImproveRunEvents(paths: EvoPaths): Promise<ImproveRunEvent[]> {\n\tlet content: string;\n\ttry {\n\t\tcontent = await readFile(getImproveRunsPath(paths), \"utf8\");\n\t} catch (error) {\n\t\tif (getErrorCode(error) === \"ENOENT\") return [];\n\t\tthrow error;\n\t}\n\n\tconst events: ImproveRunEvent[] = [];\n\tfor (const [index, line] of content.split(\"\\n\").entries()) {\n\t\tif (!line.trim()) continue;\n\t\tlet value: unknown;\n\t\ttry {\n\t\t\tvalue = JSON.parse(line) as unknown;\n\t\t} catch (error) {\n\t\t\tthrow new Error(`Invalid improve run JSON at line ${index + 1}`, { cause: error });\n\t\t}\n\t\tevents.push(parseImproveRunEvent(value, index + 1));\n\t}\n\treturn events;\n}\n\nfunction countDailyRuns(events: ImproveRunEvent[], day: string): number {\n\tconst runIds = new Set<string>();\n\tfor (const event of events) {\n\t\tif (event.localDay === day && (event.type === \"started\" || event.type === \"failed\")) runIds.add(event.runId);\n\t}\n\treturn runIds.size;\n}\n\nasync function findLatestSessionActivity(\n\tpaths: EvoPaths,\n\texcludeSessionIds: readonly string[] = [],\n): Promise<Date | undefined> {\n\tlet entries: Dirent[];\n\ttry {\n\t\tentries = await readdir(paths.log, { withFileTypes: true });\n\t} catch (error) {\n\t\tif (getErrorCode(error) === \"ENOENT\") return undefined;\n\t\tthrow error;\n\t}\n\n\tconst excluded = new Set(excludeSessionIds.map((sessionId) => `${sessionId}.jsonl`));\n\tlet latest = Number.NEGATIVE_INFINITY;\n\tfor (const entry of entries) {\n\t\tif (!entry.isFile() || !entry.name.endsWith(\".jsonl\") || excluded.has(entry.name)) continue;\n\t\ttry {\n\t\t\tlatest = Math.max(latest, (await stat(join(paths.log, entry.name))).mtimeMs);\n\t\t} catch (error) {\n\t\t\tif (getErrorCode(error) !== \"ENOENT\") throw error;\n\t\t}\n\t}\n\treturn Number.isFinite(latest) ? new Date(latest) : undefined;\n}\n\nasync function isPauseMarkerPresent(paths: EvoPaths): Promise<boolean> {\n\ttry {\n\t\tawait readFile(paths.paused, \"utf8\");\n\t\treturn true;\n\t} catch (error) {\n\t\tif (getErrorCode(error) === \"ENOENT\") return false;\n\t\tthrow error;\n\t}\n}\n\nfunction abortController(controller: AbortController, error: Error): void {\n\tif (!controller.signal.aborted) controller.abort(error);\n}\n\nfunction startPauseMonitor(paths: EvoPaths, controller: AbortController): () => Promise<void> {\n\tlet stopped = false;\n\tlet timeout: NodeJS.Timeout | undefined;\n\tlet check = Promise.resolve();\n\tconst schedule = (): void => {\n\t\tif (stopped || controller.signal.aborted) return;\n\t\ttimeout = setTimeout(() => {\n\t\t\tcheck = isPauseMarkerPresent(paths)\n\t\t\t\t.then((paused) => {\n\t\t\t\t\tif (paused) {\n\t\t\t\t\t\tabortController(controller, new Error(\"Evo-Pi was paused during scheduled reflection\"));\n\t\t\t\t\t}\n\t\t\t\t})\n\t\t\t\t.catch((error: unknown) => {\n\t\t\t\t\tabortController(controller, error instanceof Error ? error : new Error(String(error)));\n\t\t\t\t})\n\t\t\t\t.finally(schedule);\n\t\t}, PAUSE_POLL_MS);\n\t\ttimeout.unref();\n\t};\n\tschedule();\n\treturn async () => {\n\t\tstopped = true;\n\t\tif (timeout) clearTimeout(timeout);\n\t\tawait check;\n\t};\n}\n\nasync function appendImproveRunEvent(paths: EvoPaths, event: ImproveRunEvent): Promise<void> {\n\tawait appendJsonLine(getImproveRunsPath(paths), event);\n}\n\nexport async function runScheduledImprove<T>(\n\toptions: RunScheduledImproveOptions<T>,\n): Promise<ScheduledImproveResult<T>> {\n\tconst config = resolveScheduledImproveConfig(options.config);\n\tconst now = options.now ?? (() => new Date());\n\tconst startedAt = now();\n\tif (!Number.isFinite(startedAt.getTime())) throw new Error(\"now returned an invalid date\");\n\tawait ensureEvoLayout(options.paths);\n\n\tconst owner = options.createOwnerToken?.() ?? randomUUID();\n\tif (!owner) throw new Error(\"createOwnerToken returned an empty owner token\");\n\tconst controller = new AbortController();\n\tconst attempt = await tryWithFileLock<ScheduledImproveResult<T>>(\n\t\toptions.paths,\n\t\t\"scheduled-improve\",\n\t\tasync () => {\n\t\t\tawait truncateIncompleteFinalLine(getImproveRunsPath(options.paths));\n\t\t\tconst registry = new BundleRegistry(options.paths);\n\t\t\tif (await registry.isPaused()) return { status: \"skipped\", reason: \"paused\" };\n\t\t\tif (config.quietHours && !isWithinQuietHours(startedAt, config.quietHours)) {\n\t\t\t\treturn { status: \"skipped\", reason: \"outside-quiet-hours\" };\n\t\t\t}\n\n\t\t\tconst day = localDay(startedAt);\n\t\t\tconst events = await readImproveRunEvents(options.paths);\n\t\t\tconst lastCompletedDay = latestCompletedImproveDay(events);\n\t\t\tif (lastCompletedDay !== undefined) {\n\t\t\t\tconst nextEligibleDay = addLocalDays(lastCompletedDay, config.everyDays);\n\t\t\t\tif (parseLocalDayMs(day) < parseLocalDayMs(nextEligibleDay)) {\n\t\t\t\t\treturn { status: \"skipped\", reason: \"interval-not-elapsed\", nextEligibleDay };\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst lastActivity = await findLatestSessionActivity(options.paths, options.excludeSessionIds);\n\t\t\tif (lastActivity && startedAt.getTime() - lastActivity.getTime() < config.inactivityMinutes * MINUTE_MS) {\n\t\t\t\treturn {\n\t\t\t\t\tstatus: \"skipped\",\n\t\t\t\t\treason: \"recent-session-activity\",\n\t\t\t\t\tlastActivityAt: lastActivity.toISOString(),\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tif (countDailyRuns(events, day) >= config.dailyRunLimit) {\n\t\t\t\treturn { status: \"skipped\", reason: \"daily-limit-reached\" };\n\t\t\t}\n\n\t\t\tconst runId = randomUUID();\n\t\t\tawait appendImproveRunEvent(options.paths, {\n\t\t\t\tschemaVersion: SCHEDULE_SCHEMA_VERSION,\n\t\t\t\ttype: \"started\",\n\t\t\t\trunId,\n\t\t\t\ttimestamp: startedAt.toISOString(),\n\t\t\t\tlocalDay: day,\n\t\t\t});\n\n\t\t\ttry {\n\t\t\t\tconst stopPauseMonitor = startPauseMonitor(options.paths, controller);\n\t\t\t\tlet value: T;\n\t\t\t\ttry {\n\t\t\t\t\tcontroller.signal.throwIfAborted();\n\t\t\t\t\tvalue = await options.improve(controller.signal);\n\t\t\t\t\tcontroller.signal.throwIfAborted();\n\t\t\t\t} finally {\n\t\t\t\t\tawait stopPauseMonitor();\n\t\t\t\t}\n\t\t\t\tawait withFileLock(options.paths, \"registry\", async () => {\n\t\t\t\t\tif (await isPauseMarkerPresent(options.paths)) {\n\t\t\t\t\t\tabortController(controller, new Error(\"Evo-Pi was paused during scheduled reflection\"));\n\t\t\t\t\t}\n\t\t\t\t\tcontroller.signal.throwIfAborted();\n\t\t\t\t\tconst completedAt = now();\n\t\t\t\t\tif (!Number.isFinite(completedAt.getTime())) throw new Error(\"now returned an invalid date\");\n\t\t\t\t\tawait appendImproveRunEvent(options.paths, {\n\t\t\t\t\t\tschemaVersion: SCHEDULE_SCHEMA_VERSION,\n\t\t\t\t\t\ttype: \"completed\",\n\t\t\t\t\t\trunId,\n\t\t\t\t\t\ttimestamp: completedAt.toISOString(),\n\t\t\t\t\t\tlocalDay: day,\n\t\t\t\t\t});\n\t\t\t\t});\n\t\t\t\treturn { status: \"completed\", runId, value };\n\t\t\t} catch (error) {\n\t\t\t\tconst failedAt = now();\n\t\t\t\tif (!Number.isFinite(failedAt.getTime())) throw new Error(\"now returned an invalid date\");\n\t\t\t\tawait appendImproveRunEvent(options.paths, {\n\t\t\t\t\tschemaVersion: SCHEDULE_SCHEMA_VERSION,\n\t\t\t\t\ttype: \"failed\",\n\t\t\t\t\trunId,\n\t\t\t\t\ttimestamp: failedAt.toISOString(),\n\t\t\t\t\tlocalDay: day,\n\t\t\t\t\terror: errorMessage(error),\n\t\t\t\t});\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t},\n\t\t{\n\t\t\tstaleAfterMs: config.lockStaleMs,\n\t\t\townerToken: owner,\n\t\t\tafterLockPrepared: options.afterLockPrepared,\n\t\t\tonHeartbeatError: (error) => {\n\t\t\t\tabortController(controller, error instanceof Error ? error : new Error(String(error)));\n\t\t\t},\n\t\t},\n\t);\n\tif (!attempt.acquired) return { status: \"skipped\", reason: \"already-running\" };\n\treturn attempt.value;\n}\n\nexport function getScheduleConfigPath(paths: EvoPaths): string {\n\treturn join(paths.registry, \"schedule.json\");\n}\n\nfunction parseScheduleMode(value: unknown): EvoScheduleMode {\n\tif (value !== \"auto\" && value !== \"manual\") throw new Error('Schedule mode must be \"auto\" or \"manual\"');\n\treturn value;\n}\n\nfunction parseStoredQuietHours(value: unknown): QuietHours | null {\n\tif (value === null) return null;\n\tif (\n\t\ttypeof value !== \"object\" ||\n\t\tvalue === null ||\n\t\ttypeof (value as Record<string, unknown>).start !== \"string\" ||\n\t\ttypeof (value as Record<string, unknown>).end !== \"string\"\n\t) {\n\t\tthrow new Error(\"Schedule quietHours must be null or an object with start and end clocks\");\n\t}\n\treturn resolveQuietHours(value as QuietHours);\n}\n\nfunction parseScheduleConfig(value: unknown): EvoScheduleConfig {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n\t\tthrow new Error(\"Stored Evo-Pi schedule configuration is invalid\");\n\t}\n\tconst config = value as Record<string, unknown>;\n\tif (config.schemaVersion !== SCHEDULE_CONFIG_SCHEMA_VERSION) {\n\t\tthrow new Error(\"Stored Evo-Pi schedule configuration has an unsupported schema version\");\n\t}\n\tif (\n\t\ttypeof config.everyDays !== \"number\" ||\n\t\ttypeof config.inactivityMinutes !== \"number\" ||\n\t\ttypeof config.dailyRunLimit !== \"number\" ||\n\t\ttypeof config.trialDueAfterDays !== \"number\" ||\n\t\t(config.trialDueAfterSessions !== undefined && typeof config.trialDueAfterSessions !== \"number\")\n\t) {\n\t\tthrow new Error(\"Stored Evo-Pi schedule configuration is invalid\");\n\t}\n\treturn {\n\t\tschemaVersion: SCHEDULE_CONFIG_SCHEMA_VERSION,\n\t\tmode: parseScheduleMode(config.mode),\n\t\teveryDays: requireScheduleDays(config.everyDays, \"everyDays\"),\n\t\tquietHours: parseStoredQuietHours(config.quietHours),\n\t\tinactivityMinutes: requirePositiveSafeInteger(config.inactivityMinutes, \"inactivityMinutes\"),\n\t\tdailyRunLimit: requirePositiveSafeInteger(config.dailyRunLimit, \"dailyRunLimit\"),\n\t\ttrialDueAfterDays: requireScheduleDays(config.trialDueAfterDays, \"trialDueAfterDays\"),\n\t\ttrialDueAfterSessions: requirePositiveSafeInteger(\n\t\t\tconfig.trialDueAfterSessions ?? DEFAULT_EVO_SCHEDULE_CONFIG.trialDueAfterSessions,\n\t\t\t\"trialDueAfterSessions\",\n\t\t),\n\t};\n}\n\nexport async function readScheduleConfig(paths: EvoPaths): Promise<EvoScheduleConfig> {\n\tconst stored = await readJsonIfExists<unknown>(getScheduleConfigPath(paths));\n\tif (stored === undefined) return { ...DEFAULT_EVO_SCHEDULE_CONFIG };\n\treturn parseScheduleConfig(stored);\n}\n\nexport async function writeScheduleConfig(paths: EvoPaths, input: EvoScheduleConfigInput): Promise<EvoScheduleConfig> {\n\tawait ensureEvoLayout(paths);\n\treturn withFileLock(paths, \"schedule-config\", async () => {\n\t\tconst current = await readScheduleConfig(paths);\n\t\tconst next = parseScheduleConfig({\n\t\t\tschemaVersion: SCHEDULE_CONFIG_SCHEMA_VERSION,\n\t\t\tmode: input.mode ?? current.mode,\n\t\t\teveryDays: input.everyDays ?? current.everyDays,\n\t\t\tquietHours: input.quietHours === undefined ? current.quietHours : input.quietHours,\n\t\t\tinactivityMinutes: input.inactivityMinutes ?? current.inactivityMinutes,\n\t\t\tdailyRunLimit: input.dailyRunLimit ?? current.dailyRunLimit,\n\t\t\ttrialDueAfterDays: input.trialDueAfterDays ?? current.trialDueAfterDays,\n\t\t\ttrialDueAfterSessions: input.trialDueAfterSessions ?? current.trialDueAfterSessions,\n\t\t});\n\t\tawait atomicWriteJson(getScheduleConfigPath(paths), next);\n\t\treturn next;\n\t});\n}\n\nexport function parseScheduleCadence(value: string): EvoScheduleConfigInput | undefined {\n\tconst normalized = value\n\t\t.trim()\n\t\t.toLowerCase()\n\t\t.replace(/^every\\s+/, \"\");\n\tif (normalized === \"manual\" || normalized === \"off\") return { mode: \"manual\" };\n\tif (normalized === \"daily\") return { mode: \"auto\", everyDays: 1 };\n\tif (normalized === \"weekly\") return { mode: \"auto\", everyDays: 7 };\n\tconst match = /^(\\d{1,3})\\s*(?:d|day|days)?$/.exec(normalized);\n\tif (!match) return undefined;\n\tconst days = Number(match[1]);\n\tif (days < 1 || days > MAX_SCHEDULE_DAYS) return undefined;\n\treturn { mode: \"auto\", everyDays: days };\n}\n\nexport interface RunConfiguredImproveOptions<T> {\n\tpaths: EvoPaths;\n\timprove: (signal: AbortSignal) => Promise<T>;\n\tnow?: () => Date;\n\texcludeSessionIds?: readonly string[];\n\tcreateOwnerToken?: () => string;\n}\n\nexport async function runConfiguredImprove<T>(\n\toptions: RunConfiguredImproveOptions<T>,\n): Promise<ScheduledImproveResult<T>> {\n\tconst schedule = await readScheduleConfig(options.paths);\n\tif (schedule.mode === \"manual\") return { status: \"skipped\", reason: \"manual-mode\" };\n\treturn runScheduledImprove({\n\t\t...options,\n\t\tconfig: {\n\t\t\tquietHours: schedule.quietHours,\n\t\t\tinactivityMinutes: schedule.inactivityMinutes,\n\t\t\tdailyRunLimit: schedule.dailyRunLimit,\n\t\t\teveryDays: schedule.everyDays,\n\t\t},\n\t});\n}\n\nexport interface EvoScheduleStatus {\n\tconfig: EvoScheduleConfig;\n\tlastCompletedAt?: string;\n\tlastCompletedDay?: string;\n\tnextEligibleDay?: string;\n\trunsToday: number;\n}\n\nexport async function getScheduleStatus(\n\tpaths: EvoPaths,\n\tnow: () => Date = () => new Date(),\n): Promise<EvoScheduleStatus> {\n\tconst config = await readScheduleConfig(paths);\n\tconst events = await readImproveRunEvents(paths);\n\tconst lastCompleted = events.filter((event) => event.type === \"completed\").at(-1);\n\tconst lastCompletedDay = latestCompletedImproveDay(events);\n\treturn {\n\t\tconfig,\n\t\t...(lastCompleted ? { lastCompletedAt: lastCompleted.timestamp } : {}),\n\t\t...(lastCompletedDay ? { lastCompletedDay } : {}),\n\t\t...(config.mode === \"auto\" && lastCompletedDay\n\t\t\t? { nextEligibleDay: addLocalDays(lastCompletedDay, config.everyDays) }\n\t\t\t: {}),\n\t\trunsToday: countDailyRuns(events, localDay(now())),\n\t};\n}\n"]}