diff --git a/packages/junior-scheduler/migrations/0001_scheduler.sql b/packages/junior-scheduler/migrations/0001_scheduler.sql index 5ac100b15..14ba3b006 100644 --- a/packages/junior-scheduler/migrations/0001_scheduler.sql +++ b/packages/junior-scheduler/migrations/0001_scheduler.sql @@ -5,41 +5,26 @@ CREATE TABLE IF NOT EXISTS junior_scheduler_tasks ( next_run_at_ms BIGINT, run_now_at_ms BIGINT, created_at_ms BIGINT NOT NULL, - updated_at_ms BIGINT NOT NULL, - version INTEGER NOT NULL, - destination JSONB NOT NULL, - created_by JSONB NOT NULL, - conversation_access JSONB, - credential_subject JSONB, - execution_actor JSONB, - last_run_at_ms BIGINT, - original_request TEXT, - schedule JSONB NOT NULL, - status_reason TEXT, - task JSONB NOT NULL, record JSONB NOT NULL ); CREATE INDEX IF NOT EXISTS junior_scheduler_tasks_team_status_idx - ON junior_scheduler_tasks (team_id, status, created_at_ms); + ON junior_scheduler_tasks (team_id, created_at_ms, id) + WHERE status <> 'deleted'; -CREATE INDEX IF NOT EXISTS junior_scheduler_tasks_due_idx - ON junior_scheduler_tasks (status, run_now_at_ms, next_run_at_ms); +CREATE INDEX IF NOT EXISTS junior_scheduler_tasks_run_now_due_idx + ON junior_scheduler_tasks (run_now_at_ms, created_at_ms, id) + WHERE status = 'active' AND run_now_at_ms IS NOT NULL; + +CREATE INDEX IF NOT EXISTS junior_scheduler_tasks_next_run_due_idx + ON junior_scheduler_tasks (next_run_at_ms, created_at_ms, id) + WHERE status = 'active' AND next_run_at_ms IS NOT NULL; CREATE TABLE IF NOT EXISTS junior_scheduler_runs ( id TEXT PRIMARY KEY, task_id TEXT NOT NULL, status TEXT NOT NULL, - claimed_at_ms BIGINT NOT NULL, scheduled_for_ms BIGINT NOT NULL, - started_at_ms BIGINT, - completed_at_ms BIGINT, - dispatch_id TEXT, - error_message TEXT, - idempotency_key TEXT NOT NULL, - result_message_ts TEXT, - task_version INTEGER NOT NULL, - attempt INTEGER NOT NULL, record JSONB NOT NULL ); diff --git a/packages/junior-scheduler/src/db/schema.ts b/packages/junior-scheduler/src/db/schema.ts index e05e4d5a6..e2d5472be 100644 --- a/packages/junior-scheduler/src/db/schema.ts +++ b/packages/junior-scheduler/src/db/schema.ts @@ -1,11 +1,5 @@ -import { - bigint, - index, - integer, - jsonb, - pgTable, - text, -} from "drizzle-orm/pg-core"; +import { sql } from "drizzle-orm"; +import { bigint, index, jsonb, pgTable, text } from "drizzle-orm/pg-core"; import type { ScheduledRun, ScheduledTask } from "../types"; export const juniorSchedulerTasks = pgTable( @@ -17,31 +11,22 @@ export const juniorSchedulerTasks = pgTable( nextRunAtMs: bigint("next_run_at_ms", { mode: "number" }), runNowAtMs: bigint("run_now_at_ms", { mode: "number" }), createdAtMs: bigint("created_at_ms", { mode: "number" }).notNull(), - updatedAtMs: bigint("updated_at_ms", { mode: "number" }).notNull(), - version: integer("version").notNull(), - destination: jsonb("destination").notNull(), - createdBy: jsonb("created_by").notNull(), - conversationAccess: jsonb("conversation_access"), - credentialSubject: jsonb("credential_subject"), - executionActor: jsonb("execution_actor"), - lastRunAtMs: bigint("last_run_at_ms", { mode: "number" }), - originalRequest: text("original_request"), - schedule: jsonb("schedule").notNull(), - statusReason: text("status_reason"), - task: jsonb("task").notNull(), record: jsonb("record").$type().notNull(), }, (table) => [ - index("junior_scheduler_tasks_team_status_idx").on( - table.teamId, - table.status, - table.createdAtMs, - ), - index("junior_scheduler_tasks_due_idx").on( - table.status, - table.runNowAtMs, - table.nextRunAtMs, - ), + index("junior_scheduler_tasks_team_status_idx") + .on(table.teamId, table.createdAtMs, table.id) + .where(sql`${table.status} <> 'deleted'`), + index("junior_scheduler_tasks_run_now_due_idx") + .on(table.runNowAtMs, table.createdAtMs, table.id) + .where( + sql`${table.status} = 'active' AND ${table.runNowAtMs} IS NOT NULL`, + ), + index("junior_scheduler_tasks_next_run_due_idx") + .on(table.nextRunAtMs, table.createdAtMs, table.id) + .where( + sql`${table.status} = 'active' AND ${table.nextRunAtMs} IS NOT NULL`, + ), ], ); @@ -51,16 +36,7 @@ export const juniorSchedulerRuns = pgTable( id: text("id").primaryKey(), taskId: text("task_id").notNull(), status: text("status").notNull(), - claimedAtMs: bigint("claimed_at_ms", { mode: "number" }).notNull(), scheduledForMs: bigint("scheduled_for_ms", { mode: "number" }).notNull(), - startedAtMs: bigint("started_at_ms", { mode: "number" }), - completedAtMs: bigint("completed_at_ms", { mode: "number" }), - dispatchId: text("dispatch_id"), - errorMessage: text("error_message"), - idempotencyKey: text("idempotency_key").notNull(), - resultMessageTs: text("result_message_ts"), - taskVersion: integer("task_version").notNull(), - attempt: integer("attempt").notNull(), record: jsonb("record").$type().notNull(), }, (table) => [ diff --git a/packages/junior-scheduler/src/prompt.ts b/packages/junior-scheduler/src/prompt.ts index 75a782a93..e99279dfe 100644 --- a/packages/junior-scheduler/src/prompt.ts +++ b/packages/junior-scheduler/src/prompt.ts @@ -37,6 +37,8 @@ export function buildScheduledTaskRunPrompt(args: { const { run, task } = args; const destination = task.destination; const creator = sanitizeScheduledTaskPrincipal(task.createdBy); + // Older retained scheduler state predated executionActor; new tasks always + // store it explicitly as part of the task contract. const executionActor = task.executionActor ?? SCHEDULED_TASK_SYSTEM_ACTOR; if (!task.task.text?.trim()) { throw new Error("Scheduled task text is required"); @@ -55,7 +57,6 @@ export function buildScheduledTaskRunPrompt(args: { "", "", `- run_id: ${escapeXml(run.id)}`, - `- task_version: ${run.taskVersion}`, `- scheduled_for: ${new Date(run.scheduledForMs).toISOString()}`, `- running_at: ${new Date(args.nowMs).toISOString()}`, `- schedule: ${escapeXml(task.schedule.description)}`, diff --git a/packages/junior-scheduler/src/schedule-tools.ts b/packages/junior-scheduler/src/schedule-tools.ts index 11b9a7fbf..0c5e0555a 100644 --- a/packages/junior-scheduler/src/schedule-tools.ts +++ b/packages/junior-scheduler/src/schedule-tools.ts @@ -213,7 +213,6 @@ function compactTask(task: ScheduledTask): Record { run_now_at: task.runNowAtMs ? new Date(task.runNowAtMs).toISOString() : null, - version: task.version, }; } @@ -425,7 +424,6 @@ export function createSlackScheduleCreateTaskTool( task: { text: input.task, }, - version: 1, }; await schedulerStore(context).saveTask(task); @@ -572,7 +570,6 @@ export function createSlackScheduleUpdateTaskTool( recurrence, }, task: input.task ? { text: input.task } : lookup.task, - version: lookup.version + 1, }; await schedulerStore(context).saveTask(next); @@ -608,7 +605,6 @@ export function createSlackScheduleDeleteTaskTool( status: "deleted", nextRunAtMs: undefined, runNowAtMs: undefined, - version: lookup.version + 1, }; await schedulerStore(context).saveTask(next); @@ -648,7 +644,6 @@ export function createSlackScheduleRunTaskNowTool( ...lookup, updatedAtMs: nowMs, runNowAtMs: nowMs, - version: lookup.version + 1, }; await schedulerStore(context).saveTask(next); diff --git a/packages/junior-scheduler/src/store.ts b/packages/junior-scheduler/src/store.ts index f44199897..63a9dfa1c 100644 --- a/packages/junior-scheduler/src/store.ts +++ b/packages/junior-scheduler/src/store.ts @@ -85,7 +85,7 @@ const taskRecordSchema = z statusReason: z.string().optional(), task: taskSpecSchema, updatedAtMs: z.number(), - version: z.number(), + version: z.number().optional(), }) .strict(); const runRecordSchema = z @@ -96,7 +96,7 @@ const runRecordSchema = z completedAtMs: z.number().optional(), dispatchId: z.string().optional(), errorMessage: z.string().optional(), - idempotencyKey: z.string(), + idempotencyKey: z.string().optional(), resultMessageTs: z.string().optional(), scheduledForMs: z.number(), startedAtMs: z.number().optional(), @@ -109,7 +109,7 @@ const runRecordSchema = z "skipped", ]), taskId: z.string(), - taskVersion: z.number(), + taskVersion: z.number().optional(), }) .strict(); @@ -372,16 +372,13 @@ function buildScheduledRun(args: { scheduledForMs: number; task: ScheduledTask; }): ScheduledRun { - const idempotencyKey = `${args.task.id}:${args.scheduledForMs}`; return { id: buildRunId(args.task.id, args.scheduledForMs), attempt: 1, claimedAtMs: args.claimedAtMs, - idempotencyKey, scheduledForMs: args.scheduledForMs, status: "pending", taskId: args.task.id, - taskVersion: args.task.version, }; } @@ -457,13 +454,34 @@ function canFinishRun( /** Decode retained scheduler task state, skipping invalid legacy records. */ function parseStoredTask(value: unknown): ScheduledTask | undefined { const parsed = taskRecordSchema.safeParse(parseJsonRecord(value)); - return parsed.success ? parsed.data : undefined; + return parsed.success ? stripLegacyTaskFields(parsed.data) : undefined; } /** Decode retained scheduler run state, skipping invalid legacy records. */ function parseStoredRun(value: unknown): ScheduledRun | undefined { const parsed = runRecordSchema.safeParse(parseJsonRecord(value)); - return parsed.success ? parsed.data : undefined; + return parsed.success ? stripLegacyRunFields(parsed.data) : undefined; +} + +function stripLegacyTaskFields( + task: ScheduledTask & { version?: number }, +): ScheduledTask { + const { version: _version, ...current } = task; + return current; +} + +function stripLegacyRunFields( + run: ScheduledRun & { + idempotencyKey?: string; + taskVersion?: number; + }, +): ScheduledRun { + const { + idempotencyKey: _idempotencyKey, + taskVersion: _taskVersion, + ...current + } = run; + return current; } function parseJsonRecord(value: unknown): T | undefined { @@ -751,7 +769,6 @@ class PluginStateSchedulerStore implements SchedulerStore { status: nextStatus, statusReason: nextStatus === "paused" ? errorMessage : undefined, updatedAtMs: args.nowMs, - version: current.version + 1, }, current, ); @@ -937,7 +954,6 @@ class PluginStateSchedulerStore implements SchedulerStore { statusReason: args.status === "blocked" ? args.errorMessage : undefined, updatedAtMs: args.nowMs, - version: current.version + 1, }, current, ); @@ -953,7 +969,6 @@ class PluginStateSchedulerStore implements SchedulerStore { ...current, lastRunAtMs: args.run.scheduledForMs, updatedAtMs: args.nowMs, - version: current.version + 1, }, current, ); @@ -979,7 +994,6 @@ class PluginStateSchedulerStore implements SchedulerStore { statusReason: args.status === "blocked" ? args.errorMessage : undefined, updatedAtMs: args.nowMs, - version: current.version + 1, }, current, ); @@ -1028,7 +1042,7 @@ type SchedulerRunRow = { /** Decode scheduler SQL task records and reject rows unsafe for scan paths. */ function parseSqlTaskRecord(value: unknown): ScheduledTask | undefined { const parsed = taskRecordSchema.safeParse(parseJsonRecord(value)); - return parsed.success ? parsed.data : undefined; + return parsed.success ? stripLegacyTaskFields(parsed.data) : undefined; } function parseSqlTaskRow(row: SchedulerTaskRow): ScheduledTask | undefined { @@ -1038,7 +1052,7 @@ function parseSqlTaskRow(row: SchedulerTaskRow): ScheduledTask | undefined { /** Decode scheduler SQL run records and reject rows unsafe for scan paths. */ function parseSqlRunRow(row: SchedulerRunRow): ScheduledRun | undefined { const parsed = runRecordSchema.safeParse(parseJsonRecord(row.record)); - return parsed.success ? parsed.data : undefined; + return parsed.success ? stripLegacyRunFields(parsed.data) : undefined; } function json(value: unknown): string { @@ -1066,23 +1080,9 @@ INSERT INTO junior_scheduler_tasks ( next_run_at_ms, run_now_at_ms, created_at_ms, - updated_at_ms, - version, - destination, - created_by, - conversation_access, - credential_subject, - execution_actor, - last_run_at_ms, - original_request, - schedule, - status_reason, - task, record ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, - $9::jsonb, $10::jsonb, $11::jsonb, $12::jsonb, $13::jsonb, - $14, $15, $16::jsonb, $17, $18::jsonb, $19::jsonb + $1, $2, $3, $4, $5, $6, $7::jsonb ) ON CONFLICT (id) DO UPDATE SET team_id = EXCLUDED.team_id, @@ -1090,18 +1090,6 @@ ON CONFLICT (id) DO UPDATE SET next_run_at_ms = EXCLUDED.next_run_at_ms, run_now_at_ms = EXCLUDED.run_now_at_ms, created_at_ms = EXCLUDED.created_at_ms, - updated_at_ms = EXCLUDED.updated_at_ms, - version = EXCLUDED.version, - destination = EXCLUDED.destination, - created_by = EXCLUDED.created_by, - conversation_access = EXCLUDED.conversation_access, - credential_subject = EXCLUDED.credential_subject, - execution_actor = EXCLUDED.execution_actor, - last_run_at_ms = EXCLUDED.last_run_at_ms, - original_request = EXCLUDED.original_request, - schedule = EXCLUDED.schedule, - status_reason = EXCLUDED.status_reason, - task = EXCLUDED.task, record = EXCLUDED.record `, [ @@ -1111,18 +1099,6 @@ ON CONFLICT (id) DO UPDATE SET task.nextRunAtMs ?? null, task.runNowAtMs ?? null, task.createdAtMs, - task.updatedAtMs, - task.version, - json(task.destination), - json(task.createdBy), - task.conversationAccess ? json(task.conversationAccess) : null, - task.credentialSubject ? json(task.credentialSubject) : null, - task.executionActor ? json(task.executionActor) : null, - task.lastRunAtMs ?? null, - task.originalRequest ?? null, - json(task.schedule), - task.statusReason ?? null, - json(task.task), json(task), ], ); @@ -1135,52 +1111,18 @@ INSERT INTO junior_scheduler_runs ( id, task_id, status, - claimed_at_ms, scheduled_for_ms, - started_at_ms, - completed_at_ms, - dispatch_id, - error_message, - idempotency_key, - result_message_ts, - task_version, - attempt, record ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, - $9, $10, $11, $12, $13, $14::jsonb + $1, $2, $3, $4, $5::jsonb ) ON CONFLICT (id) DO UPDATE SET task_id = EXCLUDED.task_id, status = EXCLUDED.status, - claimed_at_ms = EXCLUDED.claimed_at_ms, scheduled_for_ms = EXCLUDED.scheduled_for_ms, - started_at_ms = EXCLUDED.started_at_ms, - completed_at_ms = EXCLUDED.completed_at_ms, - dispatch_id = EXCLUDED.dispatch_id, - error_message = EXCLUDED.error_message, - idempotency_key = EXCLUDED.idempotency_key, - result_message_ts = EXCLUDED.result_message_ts, - task_version = EXCLUDED.task_version, - attempt = EXCLUDED.attempt, record = EXCLUDED.record `, - [ - run.id, - run.taskId, - run.status, - run.claimedAtMs, - run.scheduledForMs, - run.startedAtMs ?? null, - run.completedAtMs ?? null, - run.dispatchId ?? null, - run.errorMessage ?? null, - run.idempotencyKey, - run.resultMessageTs ?? null, - run.taskVersion, - run.attempt, - json(run), - ], + [run.id, run.taskId, run.status, run.scheduledForMs, json(run)], ); } @@ -1439,7 +1381,6 @@ ORDER BY created_at_ms ASC, id ASC status: nextStatus, statusReason: nextStatus === "paused" ? errorMessage : undefined, updatedAtMs: args.nowMs, - version: current.version + 1, }, current, ); @@ -1612,7 +1553,6 @@ ORDER BY created_at_ms ASC, id ASC statusReason: args.status === "blocked" ? args.errorMessage : undefined, updatedAtMs: args.nowMs, - version: current.version + 1, }, current, ); @@ -1629,7 +1569,6 @@ ORDER BY created_at_ms ASC, id ASC ...current, lastRunAtMs: args.run.scheduledForMs, updatedAtMs: args.nowMs, - version: current.version + 1, }, current, ); @@ -1656,7 +1595,6 @@ ORDER BY created_at_ms ASC, id ASC statusReason: args.status === "blocked" ? args.errorMessage : undefined, updatedAtMs: args.nowMs, - version: current.version + 1, }, current, ); diff --git a/packages/junior-scheduler/src/types.ts b/packages/junior-scheduler/src/types.ts index 3b84cb42a..c4075f851 100644 --- a/packages/junior-scheduler/src/types.ts +++ b/packages/junior-scheduler/src/types.ts @@ -83,7 +83,6 @@ export interface ScheduledTask { statusReason?: string; task: ScheduledTaskSpec; updatedAtMs: number; - version: number; } export interface ScheduledRun { @@ -93,11 +92,9 @@ export interface ScheduledRun { completedAtMs?: number; dispatchId?: string; errorMessage?: string; - idempotencyKey: string; resultMessageTs?: string; scheduledForMs: number; startedAtMs?: number; status: ScheduledRunStatus; taskId: string; - taskVersion: number; } diff --git a/packages/junior/tests/component/scheduler-sql-plugin.test.ts b/packages/junior/tests/component/scheduler-sql-plugin.test.ts index b2e6d321e..ad86c007b 100644 --- a/packages/junior/tests/component/scheduler-sql-plugin.test.ts +++ b/packages/junior/tests/component/scheduler-sql-plugin.test.ts @@ -86,7 +86,6 @@ function createTask(overrides: Partial = {}): ScheduledTask { text: "Post a digest.", }, updatedAtMs: TEST_RUN_AT_MS, - version: 1, ...overrides, }; } @@ -286,7 +285,6 @@ describe("scheduler SQL plugin storage", () => { status: "active", statusReason: undefined, updatedAtMs: TEST_NOW_MS + 3, - version: task.version + 2, }); await expect( @@ -566,14 +564,8 @@ INSERT INTO junior_scheduler_tasks ( status, next_run_at_ms, created_at_ms, - updated_at_ms, - version, - destination, - created_by, - schedule, - task, record -) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) +) VALUES ($1, $2, $3, $4, $5, $6) `, [ "sched_bad_record", @@ -581,12 +573,6 @@ INSERT INTO junior_scheduler_tasks ( "active", TEST_RUN_AT_MS, TEST_RUN_AT_MS - 1, - TEST_RUN_AT_MS - 1, - 1, - JSON.stringify(task.destination), - JSON.stringify(task.createdBy), - JSON.stringify(task.schedule), - JSON.stringify(task.task), JSON.stringify({ id: "sched_bad_record" }), ], ); @@ -600,14 +586,8 @@ INSERT INTO junior_scheduler_tasks ( status, next_run_at_ms, created_at_ms, - updated_at_ms, - version, - destination, - created_by, - schedule, - task, record -) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) +) VALUES ($1, $2, $3, $4, $5, $6) `, [ "sched_bad_string_record", @@ -615,12 +595,6 @@ INSERT INTO junior_scheduler_tasks ( "active", TEST_RUN_AT_MS, TEST_RUN_AT_MS - 1, - TEST_RUN_AT_MS - 1, - 1, - JSON.stringify(task.destination), - JSON.stringify(task.createdBy), - JSON.stringify(task.schedule), - JSON.stringify(task.task), JSON.stringify("not-json"), ], ); @@ -633,23 +607,15 @@ INSERT INTO junior_scheduler_runs ( id, task_id, status, - claimed_at_ms, scheduled_for_ms, - idempotency_key, - task_version, - attempt, record -) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) +) VALUES ($1, $2, $3, $4, $5) `, [ "sched_bad_run", task.id, "pending", - TEST_NOW_MS - 120_000, TEST_RUN_AT_MS - 60_000, - "sched_bad_run", - 1, - 1, JSON.stringify({ id: "sched_bad_run" }), ], ); @@ -660,23 +626,15 @@ INSERT INTO junior_scheduler_runs ( id, task_id, status, - claimed_at_ms, scheduled_for_ms, - idempotency_key, - task_version, - attempt, record -) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) +) VALUES ($1, $2, $3, $4, $5) `, [ "sched_bad_string_run", task.id, "pending", - TEST_NOW_MS - 120_000, TEST_RUN_AT_MS - 60_000, - "sched_bad_string_run", - 1, - 1, JSON.stringify("not-json"), ], ); diff --git a/packages/junior/tests/integration/heartbeat.test.ts b/packages/junior/tests/integration/heartbeat.test.ts index a989a1d22..0d1975f7a 100644 --- a/packages/junior/tests/integration/heartbeat.test.ts +++ b/packages/junior/tests/integration/heartbeat.test.ts @@ -102,7 +102,6 @@ function createTask(overrides: Partial = {}): ScheduledTask { text: "Post a digest. Summarize the latest state.", }, updatedAtMs: nextRunAtMs, - version: 1, ...overrides, }; } diff --git a/packages/junior/tests/integration/slack-schedule-tools.test.ts b/packages/junior/tests/integration/slack-schedule-tools.test.ts index fb38981fe..c4eef0c65 100644 --- a/packages/junior/tests/integration/slack-schedule-tools.test.ts +++ b/packages/junior/tests/integration/slack-schedule-tools.test.ts @@ -582,7 +582,6 @@ describe("Slack schedule tools", () => { id: taskId, task: "Daily scheduler digest: Summarize open scheduler issues.", schedule: "Every day at 9am", - version: 2, }, }); @@ -628,7 +627,6 @@ describe("Slack schedule tools", () => { schedule: { description: "Every Monday at 9am", }, - version: 1, }); }); @@ -773,7 +771,6 @@ describe("Slack schedule tools", () => { task: { id: created.task.id, task: "Team-owned digest: Summarize open scheduler issues.", - version: 2, }, }); expect(deleted).toMatchObject({ @@ -794,7 +791,6 @@ describe("Slack schedule tools", () => { task: { text: "Team-owned digest: Summarize open scheduler issues.", }, - version: 3, }); }); @@ -946,7 +942,6 @@ describe("Slack schedule tools", () => { ...task!, nextRunAtMs: Date.parse("2026-06-08T16:00:00.000Z"), updatedAtMs: Date.parse("2026-05-26T16:00:00.000Z"), - version: task!.version + 1, }); const updated = await executeTool( @@ -987,7 +982,6 @@ describe("Slack schedule tools", () => { status: "blocked", statusReason: "Missing GitHub credentials.", updatedAtMs: Date.parse("2026-05-25T16:01:00.000Z"), - version: task!.version + 1, }); const updated = await executeTool( @@ -1025,7 +1019,6 @@ describe("Slack schedule tools", () => { ...task!, nextRunAtMs: scheduledNextRunAtMs, updatedAtMs: Date.parse("2026-05-25T16:01:00.000Z"), - version: task!.version + 1, }); const beforeMs = Date.now(); @@ -1081,7 +1074,6 @@ describe("Slack schedule tools", () => { status: "paused", statusReason: "Paused by user.", updatedAtMs: Date.parse("2026-05-25T16:01:00.000Z"), - version: task!.version + 1, }); await expect( diff --git a/specs/scheduler.md b/specs/scheduler.md index f42ccff39..791dc4253 100644 --- a/specs/scheduler.md +++ b/specs/scheduler.md @@ -135,12 +135,16 @@ The scheduler is a trusted runtime plugin and requires plugin SQL storage. Its plugin package owns the scheduler migration files under `migrations/`, and `junior upgrade` applies those migrations before scheduler storage hooks run. -The SQL store keeps task and run records in scheduler-owned tables: - -- `junior_scheduler_tasks` stores current task state, destination fields, due - timestamps, schedule metadata, and the full task JSON record. -- `junior_scheduler_runs` stores run claims, dispatch ids, terminal status, - attempt metadata, and the full run JSON record. +The SQL store keeps task and run records in scheduler-owned tables. The JSON +`record` column is the canonical durable scheduler record; scalar columns are +minimal query projections, not duplicate source-of-truth fields. + +- `junior_scheduler_tasks` stores task identity, team/status/due-time + projections for lookup and heartbeat scans, and the full task JSON record. +- `junior_scheduler_runs` stores run identity, task/status/scheduled-time + projections for idempotency and recovery scans, and the full compact run JSON + record. Run records are execution-control state, not rich result or log + storage. The scheduler store interface remains the stable boundary for tools, heartbeat, and operational reporting. Runtime hook bodies use plugin SQL through `ctx.db`;