diff --git a/apps/server/src/persistence/Migrations.ts b/apps/server/src/persistence/Migrations.ts index 6a9c57e18cc43..3012581f0fa45 100644 --- a/apps/server/src/persistence/Migrations.ts +++ b/apps/server/src/persistence/Migrations.ts @@ -83,6 +83,7 @@ import Migration0077 from "./Migrations/077_RepairBooleanSettledOverride.ts"; import Migration0078 from "./Migrations/078_DropOrchestrationEventCorrelationIndex.ts"; import Migration0079 from "./Migrations/079_DelegatedRecoveryIndexes.ts"; import Migration0080 from "./Migrations/080_ScheduledTaskThreadMode.ts"; +import Migration0081 from "./Migrations/081_ScheduledTaskImportance.ts"; const migrationEntries = [ [1, "OrchestrationEvents", Migration0001], @@ -166,6 +167,7 @@ const migrationEntries = [ [78, "DropOrchestrationEventCorrelationIndex", Migration0078], [79, "DelegatedRecoveryIndexes", Migration0079], [80, "ScheduledTaskThreadMode", Migration0080], + [81, "ScheduledTaskImportance", Migration0081], ] as const; export const migrationManifest = migrationEntries.map(([id, name]) => [id, name] as const); diff --git a/apps/server/src/persistence/Migrations/081_ScheduledTaskImportance.ts b/apps/server/src/persistence/Migrations/081_ScheduledTaskImportance.ts new file mode 100644 index 0000000000000..ed67ebceff3bf --- /dev/null +++ b/apps/server/src/persistence/Migrations/081_ScheduledTaskImportance.ts @@ -0,0 +1,9 @@ +import * as Effect from "effect/Effect"; +import * as SqlClient from "effect/sql/SqlClient"; + +export default Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + + // NULL classifies as routine: existing tasks keep the missed-run skip they had. + yield* sql`ALTER TABLE scheduled_tasks ADD COLUMN importance TEXT`; +}); diff --git a/apps/server/src/persistence/reconcileV2PreviewMigration.test.ts b/apps/server/src/persistence/reconcileV2PreviewMigration.test.ts index 84fa4ddb44c77..9e1db4fe557d4 100644 --- a/apps/server/src/persistence/reconcileV2PreviewMigration.test.ts +++ b/apps/server/src/persistence/reconcileV2PreviewMigration.test.ts @@ -60,6 +60,7 @@ describe("V2 preview upgrade", () => { [78, "DropOrchestrationEventCorrelationIndex"], [79, "DelegatedRecoveryIndexes"], [80, "ScheduledTaskThreadMode"], + [81, "ScheduledTaskImportance"], ]); assert.deepStrictEqual(yield* runMigrations(), []); assert.deepStrictEqual(yield* sql`SELECT * FROM orchestration_v2_legacy_imports`, imports); @@ -163,6 +164,7 @@ describe("V2 preview upgrade", () => { [78, "DropOrchestrationEventCorrelationIndex"], [79, "DelegatedRecoveryIndexes"], [80, "ScheduledTaskThreadMode"], + [81, "ScheduledTaskImportance"], ]); }).pipe(Effect.provide(NodeSqliteClient.layer({ filename: ":memory:" }))), ); diff --git a/apps/server/src/scheduledTasks/Schedule.test.ts b/apps/server/src/scheduledTasks/Schedule.test.ts index 0941c7392ff94..2a7933cef6739 100644 --- a/apps/server/src/scheduledTasks/Schedule.test.ts +++ b/apps/server/src/scheduledTasks/Schedule.test.ts @@ -1,7 +1,9 @@ import { describe, expect, it } from "@effect/vitest"; import * as DateTime from "effect/DateTime"; -import { isSameSchedule, nextScheduledRunAt } from "./Schedule.ts"; +import type { ScheduledTaskSchedule } from "@t3tools/contracts"; + +import { isSameSchedule, nextScheduledRunAt, skipsMissedRun } from "./Schedule.ts"; describe("scheduled task schedule calculation", () => { it("skips to the next matching fixed-time weekday", () => { @@ -83,3 +85,39 @@ describe("scheduled task schedule calculation", () => { ).toBe(false); }); }); + +describe("missed fixed-time runs", () => { + const dueAt = DateTime.makeZonedUnsafe( + { year: 2026, month: 11, day: 10, hour: 7, minute: 30, second: 0, millisecond: 0 }, + { timeZone: DateTime.zoneMakeLocal(), adjustForTimeZone: true }, + ); + const withinGrace = DateTime.add(dueAt, { minutes: 5 }); + const twoHoursLate = DateTime.add(dueAt, { hours: 2 }); + const fixed: ScheduledTaskSchedule = { type: "fixed_time", timeOfDay: "07:30" }; + + it("skips a routine fixed-time run more than the grace late", () => { + expect(skipsMissedRun({ schedule: fixed }, dueAt, withinGrace)).toBe(false); + expect(skipsMissedRun({ schedule: fixed }, dueAt, twoHoursLate)).toBe(true); + expect(skipsMissedRun({ schedule: fixed, importance: "routine" }, dueAt, twoHoursLate)).toBe( + true, + ); + }); + + it("issues an important or time-sensitive run regardless of how late its slot was", () => { + expect(skipsMissedRun({ schedule: fixed, importance: "important" }, dueAt, twoHoursLate)).toBe( + false, + ); + expect( + skipsMissedRun({ schedule: fixed, importance: "time_sensitive" }, dueAt, twoHoursLate), + ).toBe(false); + // The classification only reads on fixed-time schedules; other kinds were never skipped. + const interval: ScheduledTaskSchedule = { type: "interval", everyMs: 60_000 }; + expect( + skipsMissedRun({ schedule: interval, importance: "important" }, dueAt, twoHoursLate), + ).toBe(false); + const once: ScheduledTaskSchedule = { type: "once", runAt: "2026-11-10T07:30:00.000Z" }; + expect( + skipsMissedRun({ schedule: once, importance: "time_sensitive" }, dueAt, twoHoursLate), + ).toBe(false); + }); +}); diff --git a/apps/server/src/scheduledTasks/Schedule.ts b/apps/server/src/scheduledTasks/Schedule.ts index f8fb648aaaaa9..46e73e684318d 100644 --- a/apps/server/src/scheduledTasks/Schedule.ts +++ b/apps/server/src/scheduledTasks/Schedule.ts @@ -1,4 +1,8 @@ -import { MIN_SCHEDULED_TASK_INTERVAL_MS, type ScheduledTaskSchedule } from "@t3tools/contracts"; +import { + MIN_SCHEDULED_TASK_INTERVAL_MS, + type ScheduledTaskImportance, + type ScheduledTaskSchedule, +} from "@t3tools/contracts"; import * as DateTime from "effect/DateTime"; import * as Option from "effect/Option"; @@ -80,3 +84,26 @@ export function isMissedFixedTimeRun( if (schedule.type !== "fixed_time") return false; return DateTime.toEpochMillis(now) - DateTime.toEpochMillis(dueAt) > MISSED_FIXED_TIME_GRACE_MS; } + +/** Classifications whose runs are issued regardless of how late their slot came due. */ +const ISSUED_REGARDLESS: ReadonlySet = new Set([ + "important", + "time_sensitive", +]); + +/** + * Whether a due run is dropped as missed instead of issued. A fixed-time slot more than the grace + * late is skipped, unless the task is classified `important` or `time_sensitive`: that run is + * issued regardless — the classification for tasks whose timing matters more than the skip. + */ +export function skipsMissedRun( + task: { + readonly schedule: ScheduledTaskSchedule; + readonly importance?: ScheduledTaskImportance | undefined; + }, + dueAt: DateTime.DateTime, + now: DateTime.DateTime, +): boolean { + if (task.importance !== undefined && ISSUED_REGARDLESS.has(task.importance)) return false; + return isMissedFixedTimeRun(task.schedule, dueAt, now); +} diff --git a/apps/server/src/scheduledTasks/ScheduledTaskService.schedule.test.ts b/apps/server/src/scheduledTasks/ScheduledTaskService.schedule.test.ts index a6adcdaba9108..3b709a0cee327 100644 --- a/apps/server/src/scheduledTasks/ScheduledTaskService.schedule.test.ts +++ b/apps/server/src/scheduledTasks/ScheduledTaskService.schedule.test.ts @@ -6,7 +6,9 @@ import * as DateTime from "effect/DateTime"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; +import * as Ref from "effect/Ref"; import * as Schema from "effect/Schema"; +import * as SqlClient from "effect/sql/SqlClient"; import * as TestClock from "effect/testing/TestClock"; import * as ThreadLaunchService from "../orchestration-v2/ThreadLaunchService.ts"; @@ -234,3 +236,105 @@ it.effect( }).pipe(Effect.provide(ScheduledTaskService.layer.pipe(Layer.provide(layerDependencies)))); }).pipe(Effect.provide(SqlitePersistence.layerMemory)), ); + +it.effect( + "issues a missed fixed-time run for tasks classified important or time-sensitive, not for routine ones", + () => + Effect.gen(function* () { + const dueAt = DateTime.makeZonedUnsafe( + { year: 2026, month: 11, day: 10, hour: 9, minute: 0, second: 0, millisecond: 0 }, + { timeZone: DateTime.zoneMakeLocal(), adjustForTimeZone: true }, + ); + // One minute before the slot, as if the environment was about to sleep through it. + yield* TestClock.setTime(DateTime.toEpochMillis(dueAt) - 60_000); + const sql = yield* SqlClient.SqlClient; + const launched = yield* Ref.make([]); + // The scheduler is held: its ticks are captured and run by hand after the clock + // jumps, so no intermediate poll can see the slot while the "machine" sleeps. + const ticks = yield* Ref.make>>([]); + const layerDependencies = Layer.mergeAll( + NodeCrypto.layer, + ManagedPolicy.layerTest(), + Layer.mock(Scheduler.Scheduler)({ + register: ((_name: string, runDueWork: Effect.Effect) => + Ref.update(ticks, (all) => [...all, runDueWork])) as never, + }), + Layer.mock(ThreadLaunchService.ThreadLaunchService)({ + launch: (input) => + Ref.update(launched, (titles) => [...titles, input.title]).pipe( + Effect.as({ threadId: "thread:missed-run-classification", resumed: false } as never), + ), + }), + Layer.mock(ManagedProjectFolders.ManagedProjectFolders)({ + namedProjectsRoot: "/projects", + isScratchProject: () => Effect.succeed(false), + }), + Layer.mock(ThreadManagementService.ThreadManagementService)({}), + Layer.mock(SecretRequests.SecretRequests)({}), + ); + yield* Effect.gen(function* () { + const service = yield* ScheduledTaskService.ScheduledTaskService; + const input = (title: string, importance?: "important" | "time_sensitive") => ({ + id: `scheduled-task:missed-${title.toLowerCase()}`, + title, + prompt: `${title} prompt`, + enabled: true, + schedule: { type: "fixed_time", timeOfDay: "09:00" }, + ...(importance === undefined ? {} : { importance }), + projectId: "project-missed-run-classification", + workspaceStrategy: { type: "root" }, + modelSelection: { instanceId: "codex", model: "gpt-5.4" }, + runtimeMode: "full-access", + interactionMode: "default", + }); + const routine = yield* service.upsert(yield* decodeUpsertInput(input("Routine"))); + const important = yield* service.upsert( + yield* decodeUpsertInput(input("Important", "important")), + ); + const sensitive = yield* service.upsert( + yield* decodeUpsertInput(input("Sensitive", "time_sensitive")), + ); + const slot = DateTime.formatIso(DateTime.toUtc(dueAt)); + expect(routine.task.nextRunAt).toBe(slot); + expect(important.task.nextRunAt).toBe(slot); + expect(sensitive.task.nextRunAt).toBe(slot); + expect(important.task.importance).toBe("important"); + expect(sensitive.task.importance).toBe("time_sensitive"); + expect(routine.task.importance).toBeUndefined(); + + // Sleep two hours past the slot: past the 10-minute missed-run grace. + yield* TestClock.adjust( + DateTime.toEpochMillis(DateTime.add(dueAt, { hours: 2 })) - + (DateTime.toEpochMillis(dueAt) - 60_000), + ); + for (const tick of yield* Ref.get(ticks)) yield* tick; + + expect(yield* Ref.get(launched)).toEqual(["Important", "Sensitive"]); + const rows = yield* sql<{ + task_id: string; + last_run_status: string; + run_count: number; + next_run_at: string | null; + }>`SELECT task_id, last_run_status, run_count, next_run_at FROM scheduled_tasks ORDER BY task_id`; + const byId = new Map(rows.map((row) => [row.task_id, row])); + expect(byId.get("scheduled-task:missed-routine")).toMatchObject({ + last_run_status: "never", + run_count: 0, + }); + expect(byId.get("scheduled-task:missed-important")).toMatchObject({ + last_run_status: "succeeded", + run_count: 1, + }); + expect(byId.get("scheduled-task:missed-sensitive")).toMatchObject({ + last_run_status: "succeeded", + run_count: 1, + }); + // All three point at the next slot: the skipped one was rescheduled, not left due. + const nextRuns = new Set(rows.map((row) => row.next_run_at)); + expect(nextRuns.size).toBe(1); + expect([...nextRuns][0]).toBe( + DateTime.formatIso(DateTime.toUtc(DateTime.add(dueAt, { days: 1 }))), + ); + }).pipe(Effect.provide(ScheduledTaskService.layer.pipe(Layer.provide(layerDependencies)))); + }).pipe(Effect.provide(SqlitePersistence.layerMemory)), +); diff --git a/apps/server/src/scheduledTasks/ScheduledTaskService.ts b/apps/server/src/scheduledTasks/ScheduledTaskService.ts index 4953cec766210..feb60defbdbe0 100644 --- a/apps/server/src/scheduledTasks/ScheduledTaskService.ts +++ b/apps/server/src/scheduledTasks/ScheduledTaskService.ts @@ -57,7 +57,7 @@ import * as SecretRequests from "../secrets/SecretRequests.ts"; import * as ManagedPolicy from "../policy/ManagedPolicy.ts"; import * as DueHint from "../scheduling/DueHint.ts"; import * as Scheduler from "../scheduling/Scheduler.ts"; -import { isMissedFixedTimeRun, isSameSchedule, nextScheduledRunAt } from "./Schedule.ts"; +import { isSameSchedule, nextScheduledRunAt, skipsMissedRun } from "./Schedule.ts"; import { redactHeaders, redactQuery, @@ -212,6 +212,7 @@ interface ScheduledTaskRow { readonly prompt: string; readonly enabled: number; readonly schedule_json: string; + readonly importance: string | null; readonly project_id: string; readonly thread_id: string | null; readonly thread_mode: string; @@ -386,6 +387,10 @@ const decodeRow = Effect.fnUntraced( prompt: row.prompt, enabled: row.enabled === 1, schedule, + // An unknown stored value reads as routine instead of making the row undecodable. + ...(row.importance === "important" || row.importance === "time_sensitive" + ? { importance: row.importance } + : {}), projectId: row.project_id, threadId: row.thread_id, threadMode: row.thread_mode, @@ -509,6 +514,7 @@ export const layer = Layer.effect( prompt, enabled, schedule_json, + importance, project_id, thread_id, thread_mode, @@ -551,6 +557,7 @@ export const layer = Layer.effect( prompt, enabled, schedule_json, + importance, project_id, thread_id, thread_mode, @@ -629,6 +636,7 @@ export const layer = Layer.effect( prompt, enabled, schedule_json, + importance, project_id, thread_id, thread_mode, @@ -654,6 +662,7 @@ export const layer = Layer.effect( ${task.prompt}, ${task.enabled ? 1 : 0}, ${JSON.stringify(task.schedule)}, + ${task.importance ?? null}, ${task.projectId}, ${task.threadId}, ${scheduledTaskThreadMode(task)}, @@ -680,6 +689,7 @@ export const layer = Layer.effect( prompt = excluded.prompt, enabled = excluded.enabled, schedule_json = excluded.schedule_json, + importance = excluded.importance, project_id = excluded.project_id, -- A first run may bind the thread while this save is in flight; only a -- switch to new threads unbinds. @@ -1168,7 +1178,7 @@ export const layer = Layer.effect( due, ({ task, dueAt }) => Effect.suspend(() => - isMissedFixedTimeRun(task.schedule, dueAt, now) + skipsMissedRun(task, dueAt, now) ? rescheduleMissedRun(task, now) : runTask(task, "scheduled"), ).pipe( @@ -1331,6 +1341,9 @@ export const layer = Layer.effect( // Keep the existing next_run_at when the schedule itself is untouched: // editing a title or prompt must not postpone (or resurrect) a due // run — only schedule/enabled changes restart the clock. + + // Omitted keeps the stored classification, as webhook filters do; a new task starts routine. + const importance = input.importance ?? existingTask?.importance; const schedule: ScheduledTask["schedule"] = input.schedule.type === "webhook" ? { @@ -1415,6 +1428,7 @@ export const layer = Layer.effect( prompt: input.prompt, enabled: input.enabled, schedule, + ...(importance === undefined ? {} : { importance }), projectId: input.projectId, threadId, threadMode, diff --git a/apps/web/src/components/settings/ScheduledTasksSettings.tsx b/apps/web/src/components/settings/ScheduledTasksSettings.tsx index ff024e5ba2ed2..9a6e9d906432d 100644 --- a/apps/web/src/components/settings/ScheduledTasksSettings.tsx +++ b/apps/web/src/components/settings/ScheduledTasksSettings.tsx @@ -21,6 +21,7 @@ import type { ProjectId, ScheduledTask, ScheduledTaskId, + ScheduledTaskImportance, ScheduledTaskSchedule, ScheduledTaskThreadMode, ScheduledTaskUpsertInput, @@ -177,6 +178,7 @@ const EMPTY_DRAFT: DraftState = { projectId: "", threadId: "", threadMode: "new_thread", + importance: "routine", workspaceMode: "worktree", baseRef: "main", startFromOrigin: true, @@ -1198,6 +1200,12 @@ const THREAD_MODE_LABELS: Record = { fresh_session: "One thread, fresh session each run", }; +const IMPORTANCE_LABELS: Record = { + routine: "Routine", + important: "Important", + time_sensitive: "Time-sensitive", +}; + function ScheduledTaskDestination({ environmentId, projectId, @@ -1537,6 +1545,8 @@ function ScheduledTaskEditorForm({ projectId: selectedProjectId as ProjectId, threadId: draft.threadId ? (draft.threadId as ThreadId) : null, ...(offersThreadModes ? { threadMode: draft.threadMode } : {}), + // Only a fixed-time slot can be missed; other schedules keep whatever is stored. + ...(schedule.type === "fixed_time" ? { importance: draft.importance } : {}), workspaceStrategy, modelSelection, // Tasks have no mode picker; a mode above the administrator's ceiling would fail every run. @@ -2057,41 +2067,78 @@ function ScheduledTaskEditorForm({ /> ) : draft.scheduleMode === "fixed" ? ( -
-
- - - setDraft((current) => ({ ...current, timeOfDay: event.target.value })) - } - /> - on +
+
+
+ + + setDraft((current) => ({ ...current, timeOfDay: event.target.value })) + } + /> + on +
+ { + if (values.length === 0) return; + setDraft((current) => ({ + ...current, + weekdays: new Set(values.map(Number)), + })); + }} + > + {WEEKDAY_ORDER.map((day) => ( + + {WEEKDAY_SHORT[day]} + + ))} + +
+
+ + + +

+ Important and time-sensitive runs start when the environment comes back, however + late the slot passed while it was closed. Routine skips a slot missed by more + than 10 minutes. +

- { - if (values.length === 0) return; - setDraft((current) => ({ - ...current, - weekdays: new Set(values.map(Number)), - })); - }} - > - {WEEKDAY_ORDER.map((day) => ( - - {WEEKDAY_SHORT[day]} - - ))} -
) : (
diff --git a/apps/web/src/components/settings/scheduledTasksSettings.logic.test.ts b/apps/web/src/components/settings/scheduledTasksSettings.logic.test.ts index 807b10b8cc5e2..460c6f108d38d 100644 --- a/apps/web/src/components/settings/scheduledTasksSettings.logic.test.ts +++ b/apps/web/src/components/settings/scheduledTasksSettings.logic.test.ts @@ -172,6 +172,16 @@ describe("editing scheduled task branch settings", () => { }); }); +describe("missed-run classification", () => { + it("reads the stored classification and defaults to routine", () => { + expect(taskToDraft(legacyTask).importance).toBe("routine"); + expect(taskToDraft({ ...legacyTask, importance: "important" }).importance).toBe("important"); + expect(taskToDraft({ ...legacyTask, importance: "time_sensitive" }).importance).toBe( + "time_sensitive", + ); + }); +}); + describe("scheduled task destination", () => { it("unbinding leaves the shared checkout for a new worktree from the remote", () => { const rootBound = taskToDraft({ diff --git a/apps/web/src/components/settings/scheduledTasksSettings.logic.ts b/apps/web/src/components/settings/scheduledTasksSettings.logic.ts index 8d6a29a6d1857..460d43200fd82 100644 --- a/apps/web/src/components/settings/scheduledTasksSettings.logic.ts +++ b/apps/web/src/components/settings/scheduledTasksSettings.logic.ts @@ -5,6 +5,7 @@ import { type ProjectId, ScheduledTaskId, type ScheduledTask, + type ScheduledTaskImportance, type ScheduledTaskThreadMode, type ScheduledTaskUpsertSchedule, scheduledTaskThreadMode, @@ -75,6 +76,8 @@ export interface DraftState { readonly projectId: string; readonly threadId: string; readonly threadMode: ScheduledTaskThreadMode; + /** Missed-run classification: `important` and `time_sensitive` runs issue regardless of lateness. */ + readonly importance: ScheduledTaskImportance; readonly workspaceMode: WorkspaceMode; readonly baseRef: string; readonly startFromOrigin: boolean; @@ -260,6 +263,7 @@ export function taskToDraft(task: ScheduledTask): DraftState { projectId: task.projectId, threadId: task.threadId ?? "", threadMode: scheduledTaskThreadMode(task), + importance: task.importance ?? "routine", workspaceMode: task.workspaceStrategy.type, baseRef: task.workspaceStrategy.type === "worktree" ? task.workspaceStrategy.baseRef : "main", startFromOrigin: diff --git a/docs/user/project-settings.md b/docs/user/project-settings.md index 8880dcb3e64ca..b17f388efe78b 100644 --- a/docs/user/project-settings.md +++ b/docs/user/project-settings.md @@ -86,6 +86,12 @@ environment's time zone, which may differ from your phone's. Choose **Once** to run a task a single time at a date and time in your own time zone. If the environment is offline then, the task runs when it is back. +A fixed-time slot that passes while the environment is closed or asleep is +skipped. Under **Classification**, mark such a task **Important** or +**Time-sensitive** and its missed slot runs when the environment comes back, +however late; **Routine** (the default) keeps the skip. The classification +only applies to fixed-time schedules. + Under **Destination**, choose where runs go. **New thread each run** keeps every run separate. The two one-thread choices start a thread on the first run and post later runs there: one continues the same conversation, the other starts each run diff --git a/packages/contracts/src/scheduledTask.ts b/packages/contracts/src/scheduledTask.ts index 2a77777a9c3dc..314b53ceb68d3 100644 --- a/packages/contracts/src/scheduledTask.ts +++ b/packages/contracts/src/scheduledTask.ts @@ -231,6 +231,14 @@ export type ScheduledTaskWebhookEndpoint = typeof ScheduledTaskWebhookEndpoint.T export const ScheduledTaskThreadMode = Schema.Literals(["new_thread", "continue", "fresh_session"]); export type ScheduledTaskThreadMode = typeof ScheduledTaskThreadMode.Type; +/** + * Classification of a task's timing importance. `important` and `time_sensitive` tasks are issued + * regardless: their fixed-time run starts when the environment comes back, however late the slot + * passed while it was closed or asleep. `routine` (the default) keeps the missed-run skip. + */ +export const ScheduledTaskImportance = Schema.Literals(["routine", "important", "time_sensitive"]); +export type ScheduledTaskImportance = typeof ScheduledTaskImportance.Type; + /** Servers before thread modes omit it; their bound tasks continue the conversation. */ export function scheduledTaskThreadMode( task: Pick, @@ -244,6 +252,8 @@ export const ScheduledTask = Schema.Struct({ prompt: TrimmedNonEmptyString, enabled: Schema.Boolean, schedule: ScheduledTaskSchedule, + /** Classification of the task's timing importance. Omitted: `routine`. */ + importance: Schema.optional(ScheduledTaskImportance), projectId: ProjectId, threadId: Schema.NullOr(ThreadId), threadMode: Schema.optional(ScheduledTaskThreadMode), @@ -284,6 +294,12 @@ export const ScheduledTaskUpsertInput = Schema.Struct({ prompt: TrimmedNonEmptyString, enabled: Schema.Boolean, schedule: ScheduledTaskUpsertSchedule, + /** + * Classification of the task's timing importance: `important` and `time_sensitive` runs are + * issued regardless of how late their slot was. Omitted: keep the stored one (`routine` for a + * new task). + */ + importance: Schema.optional(ScheduledTaskImportance), projectId: ProjectId, threadId: Schema.optional(Schema.NullOr(ThreadId)), /** Omitted: keep the stored mode, or derive it from `threadId` for a new or rebound task. */