From 2b6d7d4ec269c793f77292c4bc2265824e15d250 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Mon, 4 May 2026 16:37:33 +0800 Subject: [PATCH] schedule: fire --every now by default, add run-once for cron-style triggers (#689) Interval schedules used to wait the full interval before their first run, which made `--every 1d` feel broken for fresh schedules. Now `--every` fires immediately on creation; `--cron` keeps waiting for the next slot. `--no-run-now` and `--run-now` override the defaults, and the parser rejects redundant combos. Adds `paseo schedule run-once ` to manually trigger a single run without advancing cadence or recomputing completion. The new `runOnCreate` field and `schedule/run-once` RPC are additive on the WebSocket schema. --- packages/cli/src/commands/schedule/create.ts | 6 +- packages/cli/src/commands/schedule/index.ts | 10 ++ .../cli/src/commands/schedule/run-once.ts | 33 ++++ .../cli/src/commands/schedule/shared.test.ts | 46 ++++++ packages/cli/src/commands/schedule/shared.ts | 25 +++ packages/cli/src/commands/schedule/types.ts | 8 + packages/server/src/client/daemon-client.ts | 18 +++ .../server/src/server/schedule/rpc-schemas.ts | 16 ++ .../src/server/schedule/service.test.ts | 151 ++++++++++++++++++ .../server/src/server/schedule/service.ts | 32 +++- packages/server/src/server/schedule/types.ts | 1 + packages/server/src/server/session.ts | 51 ++++-- packages/server/src/shared/messages.ts | 6 + 13 files changed, 387 insertions(+), 16 deletions(-) create mode 100644 packages/cli/src/commands/schedule/run-once.ts diff --git a/packages/cli/src/commands/schedule/create.ts b/packages/cli/src/commands/schedule/create.ts index 957722b70..c0e47ce74 100644 --- a/packages/cli/src/commands/schedule/create.ts +++ b/packages/cli/src/commands/schedule/create.ts @@ -20,13 +20,16 @@ export interface ScheduleCreateOptions extends ScheduleCommandOptions { cwd?: string; maxRuns?: string; expiresIn?: string; + runNow?: boolean; } export async function runCreateCommand( prompt: string, options: ScheduleCreateOptions, - _command: Command, + command: Command, ): Promise> { + const runNowSource = command.getOptionValueSource("runNow"); + const runNow = runNowSource === "cli" ? Boolean(options.runNow) : undefined; const input = parseScheduleCreateInput({ prompt, every: options.every, @@ -39,6 +42,7 @@ export async function runCreateCommand( host: options.host, maxRuns: options.maxRuns, expiresIn: options.expiresIn, + runNow, }); const { client } = await connectScheduleClient(options.host); try { diff --git a/packages/cli/src/commands/schedule/index.ts b/packages/cli/src/commands/schedule/index.ts index 9caee32d8..57bf42881 100644 --- a/packages/cli/src/commands/schedule/index.ts +++ b/packages/cli/src/commands/schedule/index.ts @@ -8,6 +8,7 @@ import { runLogsCommand } from "./logs.js"; import { runPauseCommand } from "./pause.js"; import { runResumeCommand } from "./resume.js"; import { runDeleteCommand } from "./delete.js"; +import { runRunOnceCommand } from "./run-once.js"; export function createScheduleCommand(): Command { const schedule = new Command("schedule").description("Manage recurring schedules"); @@ -30,6 +31,8 @@ export function createScheduleCommand(): Command { "Provider-specific mode (e.g. claude bypassPermissions, opencode build)", ) .option("--cwd ", "Working directory (default: current; required with --host)") + .option("--run-now", "Fire one immediate run on creation (only with --cron)") + .option("--no-run-now", "Wait the full interval before the first run (only with --every)") .option("--max-runs ", "Maximum number of runs") .option("--expires-in ", "Time to live for the schedule"), ).action(withOutput(runCreateCommand)); @@ -64,5 +67,12 @@ export function createScheduleCommand(): Command { schedule.command("delete").description("Delete a schedule").argument("", "Schedule ID"), ).action(withOutput(runDeleteCommand)); + addJsonAndDaemonHostOptions( + schedule + .command("run-once") + .description("Manually trigger a single run of a schedule without affecting cadence") + .argument("", "Schedule ID"), + ).action(withOutput(runRunOnceCommand)); + return schedule; } diff --git a/packages/cli/src/commands/schedule/run-once.ts b/packages/cli/src/commands/schedule/run-once.ts new file mode 100644 index 000000000..705e9548f --- /dev/null +++ b/packages/cli/src/commands/schedule/run-once.ts @@ -0,0 +1,33 @@ +import type { Command } from "commander"; +import type { SingleResult } from "../../output/index.js"; +import { scheduleSchema } from "./schema.js"; +import { + connectScheduleClient, + toScheduleCommandError, + toScheduleRow, + type ScheduleCommandOptions, + type ScheduleRow, +} from "./shared.js"; + +export async function runRunOnceCommand( + id: string, + options: ScheduleCommandOptions, + _command: Command, +): Promise> { + const { client } = await connectScheduleClient(options.host); + try { + const payload = await client.scheduleRunOnce({ id }); + if (payload.error || !payload.schedule) { + throw new Error(payload.error ?? `Failed to run schedule once: ${id}`); + } + return { + type: "single", + data: toScheduleRow(payload.schedule), + schema: scheduleSchema, + }; + } catch (error) { + throw toScheduleCommandError("SCHEDULE_RUN_ONCE_FAILED", "run schedule once", error); + } finally { + await client.close().catch(() => {}); + } +} diff --git a/packages/cli/src/commands/schedule/shared.test.ts b/packages/cli/src/commands/schedule/shared.test.ts index 81daab83f..9eda718cf 100644 --- a/packages/cli/src/commands/schedule/shared.test.ts +++ b/packages/cli/src/commands/schedule/shared.test.ts @@ -8,6 +8,12 @@ const baseOptions = { provider: "claude", }; +const baseCron = { + prompt: "do the thing", + cron: "0 9 * * *", + provider: "claude", +}; + describe("parseScheduleCreateInput cwd/host validation", () => { beforeEach(() => { vi.spyOn(process, "cwd").mockReturnValue("/local/project"); @@ -60,3 +66,43 @@ describe("parseScheduleCreateInput cwd/host validation", () => { ).toThrow(expect.objectContaining({ code: "MISSING_CWD" })); }); }); + +describe("parseScheduleCreateInput first-run timing", () => { + test("--every with no run-now flag fires immediately on creation", () => { + const input = parseScheduleCreateInput(baseOptions); + expect(input.runOnCreate).toBe(true); + }); + + test("--every with --no-run-now waits the interval", () => { + const input = parseScheduleCreateInput({ ...baseOptions, runNow: false }); + expect(input.runOnCreate).toBe(false); + }); + + test("--cron with no run-now flag waits for the next cron slot", () => { + const input = parseScheduleCreateInput(baseCron); + expect(input.runOnCreate).toBe(false); + }); + + test("--cron with --run-now fires immediately on creation", () => { + const input = parseScheduleCreateInput({ ...baseCron, runNow: true }); + expect(input.runOnCreate).toBe(true); + }); + + test("--every with --run-now is rejected as redundant", () => { + expect(() => parseScheduleCreateInput({ ...baseOptions, runNow: true })).toThrow( + expect.objectContaining({ + code: "REDUNDANT_RUN_NOW", + message: expect.stringContaining("--run-now is redundant with --every"), + }), + ); + }); + + test("--cron with --no-run-now is rejected as redundant", () => { + expect(() => parseScheduleCreateInput({ ...baseCron, runNow: false })).toThrow( + expect.objectContaining({ + code: "REDUNDANT_NO_RUN_NOW", + message: expect.stringContaining("--no-run-now is redundant with --cron"), + }), + ); + }); +}); diff --git a/packages/cli/src/commands/schedule/shared.ts b/packages/cli/src/commands/schedule/shared.ts index 9e59d4944..0f55de5df 100644 --- a/packages/cli/src/commands/schedule/shared.ts +++ b/packages/cli/src/commands/schedule/shared.ts @@ -135,6 +135,7 @@ export function parseScheduleCreateInput(options: { host?: string; maxRuns?: string; expiresIn?: string; + runNow?: boolean; }): CreateScheduleInput { const prompt = options.prompt.trim(); if (!prompt) { @@ -165,6 +166,8 @@ export function parseScheduleCreateInput(options: { } satisfies CommandError; } + const runOnCreate = resolveRunOnCreate(options.runNow, cadence.type); + const targetValue = options.target?.trim(); const modeId = options.mode?.trim(); const hasExplicitNewAgentOption = options.provider !== undefined || options.mode !== undefined; @@ -199,12 +202,34 @@ export function parseScheduleCreateInput(options: { prompt, cadence, target, + runOnCreate, ...(options.name?.trim() ? { name: options.name.trim() } : {}), ...(maxRuns !== undefined ? { maxRuns } : {}), ...(expiresAt ? { expiresAt } : {}), }; } +function resolveRunOnCreate( + runNow: boolean | undefined, + cadenceType: ScheduleCadence["type"], +): boolean { + if (runNow === true && cadenceType === "every") { + throw { + code: "REDUNDANT_RUN_NOW", + message: "--run-now is redundant with --every (interval schedules already fire on creation)", + details: "Drop --run-now, or use --no-run-now to wait the full interval before the first run", + } satisfies CommandError; + } + if (runNow === false && cadenceType === "cron") { + throw { + code: "REDUNDANT_NO_RUN_NOW", + message: "--no-run-now is redundant with --cron (cron schedules never fire on creation)", + details: "Drop --no-run-now, or use --run-now to fire one immediate run on creation", + } satisfies CommandError; + } + return runNow ?? cadenceType === "every"; +} + function parsePositiveInt(value: string, flag: string): number { const parsed = Number.parseInt(value, 10); if (!Number.isInteger(parsed) || parsed <= 0) { diff --git a/packages/cli/src/commands/schedule/types.ts b/packages/cli/src/commands/schedule/types.ts index 6f9a27b74..e4d9b5d8d 100644 --- a/packages/cli/src/commands/schedule/types.ts +++ b/packages/cli/src/commands/schedule/types.ts @@ -85,6 +85,7 @@ export interface CreateScheduleInput { target: ScheduleTarget; maxRuns?: number; expiresAt?: string; + runOnCreate?: boolean; } export interface ScheduleCreatePayload { @@ -129,6 +130,12 @@ export interface ScheduleDeletePayload { error: string | null; } +export interface ScheduleRunOncePayload { + requestId: string; + schedule: ScheduleRecord | null; + error: string | null; +} + export interface ScheduleDaemonClient { scheduleCreate(input: CreateScheduleInput): Promise; scheduleList(): Promise; @@ -137,5 +144,6 @@ export interface ScheduleDaemonClient { schedulePause(input: { id: string }): Promise; scheduleResume(input: { id: string }): Promise; scheduleDelete(input: { id: string }): Promise; + scheduleRunOnce(input: { id: string }): Promise; close(): Promise; } diff --git a/packages/server/src/client/daemon-client.ts b/packages/server/src/client/daemon-client.ts index 6d96dd31b..d99571f90 100644 --- a/packages/server/src/client/daemon-client.ts +++ b/packages/server/src/client/daemon-client.ts @@ -388,6 +388,10 @@ type ScheduleDeletePayload = Extract< SessionOutboundMessage, { type: "schedule/delete/response" } >["payload"]; +type ScheduleRunOncePayload = Extract< + SessionOutboundMessage, + { type: "schedule/run-once/response" } +>["payload"]; export type FetchAgentTimelinePayload = FetchAgentTimelineResponseMessage["payload"]; export type FetchAgentTimelineDirection = FetchAgentTimelinePayload["direction"]; @@ -541,6 +545,7 @@ export interface CreateScheduleOptions { }; maxRuns?: number; expiresAt?: string; + runOnCreate?: boolean; requestId?: string; } export interface InspectScheduleOptions { @@ -3667,6 +3672,7 @@ export class DaemonClient { ...(options.name ? { name: options.name } : {}), ...(typeof options.maxRuns === "number" ? { maxRuns: options.maxRuns } : {}), ...(options.expiresAt ? { expiresAt: options.expiresAt } : {}), + ...(typeof options.runOnCreate === "boolean" ? { runOnCreate: options.runOnCreate } : {}), }, responseType: "schedule/create/response", timeout: 10000, @@ -3744,6 +3750,18 @@ export class DaemonClient { }); } + async scheduleRunOnce(options: InspectScheduleOptions): Promise { + return this.sendCorrelatedSessionRequest({ + requestId: options.requestId, + message: { + type: "schedule/run-once", + scheduleId: options.id, + }, + responseType: "schedule/run-once/response", + timeout: 10000, + }); + } + async loopRun(options: RunLoopOptions): Promise { return this.sendCorrelatedSessionRequest({ requestId: options.requestId, diff --git a/packages/server/src/server/schedule/rpc-schemas.ts b/packages/server/src/server/schedule/rpc-schemas.ts index 5c70e2585..e6cdb0070 100644 --- a/packages/server/src/server/schedule/rpc-schemas.ts +++ b/packages/server/src/server/schedule/rpc-schemas.ts @@ -31,6 +31,7 @@ export const ScheduleCreateRequestSchema = z.object({ target: ScheduleCreateTargetSchema, maxRuns: z.number().int().positive().optional(), expiresAt: z.string().optional(), + runOnCreate: z.boolean().optional(), }); export const ScheduleListRequestSchema = z.object({ @@ -68,6 +69,12 @@ export const ScheduleDeleteRequestSchema = z.object({ scheduleId: z.string(), }); +export const ScheduleRunOnceRequestSchema = z.object({ + type: z.literal("schedule/run-once"), + requestId: z.string(), + scheduleId: z.string(), +}); + export const ScheduleCreateResponseSchema = z.object({ type: z.literal("schedule/create/response"), payload: z.object({ @@ -130,3 +137,12 @@ export const ScheduleDeleteResponseSchema = z.object({ error: z.string().nullable(), }), }); + +export const ScheduleRunOnceResponseSchema = z.object({ + type: z.literal("schedule/run-once/response"), + payload: z.object({ + requestId: z.string(), + schedule: StoredScheduleSchema.nullable(), + error: z.string().nullable(), + }), +}); diff --git a/packages/server/src/server/schedule/service.test.ts b/packages/server/src/server/schedule/service.test.ts index 2bb199d9c..27fc8150c 100644 --- a/packages/server/src/server/schedule/service.test.ts +++ b/packages/server/src/server/schedule/service.test.ts @@ -231,6 +231,7 @@ describe("ScheduleService", () => { type: "new-agent", config: { provider: "claude", cwd: tempDir }, }, + runOnCreate: false, }); expect(created.nextRunAt).toBe("2026-01-01T00:01:00.000Z"); @@ -366,4 +367,154 @@ describe("ScheduleService", () => { }), ).rejects.toThrow("Agent archived-agent is archived"); }); + + test("defaults --every schedules to fire immediately on creation", async () => { + const service = new ScheduleService({ + paseoHome: tempDir, + logger: createTestLogger(), + agentManager: new AgentManager({ logger: createTestLogger() }), + agentStorage, + now: () => now, + runner: async () => ({ agentId: null, output: "ok" }), + }); + + const created = await service.create({ + prompt: "every default", + cadence: { type: "every", everyMs: 60_000 }, + target: { + type: "new-agent", + config: { provider: "claude", cwd: tempDir }, + }, + }); + + expect(created.nextRunAt).toBe(now.toISOString()); + }); + + test("--every with runOnCreate=false waits the full interval", async () => { + const service = new ScheduleService({ + paseoHome: tempDir, + logger: createTestLogger(), + agentManager: new AgentManager({ logger: createTestLogger() }), + agentStorage, + now: () => now, + runner: async () => ({ agentId: null, output: "ok" }), + }); + + const created = await service.create({ + prompt: "wait interval", + cadence: { type: "every", everyMs: 60_000 }, + target: { + type: "new-agent", + config: { provider: "claude", cwd: tempDir }, + }, + runOnCreate: false, + }); + + expect(created.nextRunAt).toBe("2026-01-01T00:01:00.000Z"); + }); + + test("--cron defaults to the next cron slot", async () => { + const service = new ScheduleService({ + paseoHome: tempDir, + logger: createTestLogger(), + agentManager: new AgentManager({ logger: createTestLogger() }), + agentStorage, + now: () => now, + runner: async () => ({ agentId: null, output: "ok" }), + }); + + const created = await service.create({ + prompt: "cron default", + cadence: { type: "cron", expression: "30 9 * * *" }, + target: { + type: "new-agent", + config: { provider: "claude", cwd: tempDir }, + }, + }); + + expect(created.nextRunAt).toBe("2026-01-01T09:30:00.000Z"); + }); + + test("--cron with runOnCreate=true fires immediately on creation", async () => { + const service = new ScheduleService({ + paseoHome: tempDir, + logger: createTestLogger(), + agentManager: new AgentManager({ logger: createTestLogger() }), + agentStorage, + now: () => now, + runner: async () => ({ agentId: null, output: "ok" }), + }); + + const created = await service.create({ + prompt: "cron run-now", + cadence: { type: "cron", expression: "30 9 * * *" }, + target: { + type: "new-agent", + config: { provider: "claude", cwd: tempDir }, + }, + runOnCreate: true, + }); + + expect(created.nextRunAt).toBe(now.toISOString()); + }); + + test("runOnce records a run without changing nextRunAt or completing the schedule", async () => { + const service = new ScheduleService({ + paseoHome: tempDir, + logger: createTestLogger(), + agentManager: new AgentManager({ logger: createTestLogger() }), + agentStorage, + now: () => now, + runner: async (schedule) => ({ + agentId: "00000000-0000-0000-0000-000000000099", + output: `manual:${schedule.prompt}`, + }), + }); + + const created = await service.create({ + prompt: "manual fire", + cadence: { type: "cron", expression: "30 9 * * *" }, + target: { + type: "new-agent", + config: { provider: "claude", cwd: tempDir }, + }, + maxRuns: 1, + }); + expect(created.nextRunAt).toBe("2026-01-01T09:30:00.000Z"); + + const after = await service.runOnce(created.id); + expect(after.nextRunAt).toBe("2026-01-01T09:30:00.000Z"); + expect(after.status).toBe("active"); + expect(after.runs).toHaveLength(1); + expect(after.runs[0]).toMatchObject({ + status: "succeeded", + agentId: "00000000-0000-0000-0000-000000000099", + output: "manual:manual fire", + }); + }); + + test("runOnce rejects completed schedules", async () => { + const service = new ScheduleService({ + paseoHome: tempDir, + logger: createTestLogger(), + agentManager: new AgentManager({ logger: createTestLogger() }), + agentStorage, + now: () => now, + runner: async () => ({ agentId: null, output: "ok" }), + }); + + const created = await service.create({ + prompt: "one-shot", + cadence: { type: "every", everyMs: 60_000 }, + target: { + type: "new-agent", + config: { provider: "claude", cwd: tempDir }, + }, + maxRuns: 1, + }); + now = new Date("2026-01-01T00:01:00.000Z"); + await service.tick(); + + await expect(service.runOnce(created.id)).rejects.toThrow("already completed"); + }); }); diff --git a/packages/server/src/server/schedule/service.ts b/packages/server/src/server/schedule/service.ts index f77e12b85..7d913da82 100644 --- a/packages/server/src/server/schedule/service.ts +++ b/packages/server/src/server/schedule/service.ts @@ -142,6 +142,8 @@ export class ScheduleService { const now = this.now(); const prompt = normalizePrompt(input.prompt); validateScheduleCadence(input.cadence); + const runOnCreate = input.runOnCreate ?? input.cadence.type === "every"; + const nextRunAt = runOnCreate ? now : computeNextRunAt(input.cadence, now); const schedule = await this.store.create({ name: trimOptionalName(input.name), prompt, @@ -150,7 +152,7 @@ export class ScheduleService { status: "active", createdAt: now.toISOString(), updatedAt: now.toISOString(), - nextRunAt: computeNextRunAt(input.cadence, now).toISOString(), + nextRunAt: nextRunAt.toISOString(), lastRunAt: null, pausedAt: null, expiresAt: input.expiresAt ?? null, @@ -221,6 +223,18 @@ export class ScheduleService { await this.store.delete(id); } + async runOnce(id: string): Promise { + const schedule = await this.inspect(id); + if (schedule.status === "completed") { + throw new Error(`Schedule ${id} is already completed`); + } + if (this.runningScheduleIds.has(id)) { + throw new Error(`Schedule ${id} is already running`); + } + await this.runSchedule(schedule, this.now(), { manual: true }); + return this.inspect(id); + } + async tick(): Promise { const now = this.now(); const schedules = await this.store.list(); @@ -286,12 +300,17 @@ export class ScheduleService { ); } - private async runSchedule(schedule: StoredSchedule, now: Date): Promise { + private async runSchedule( + schedule: StoredSchedule, + now: Date, + options?: { manual?: boolean }, + ): Promise { + const manual = options?.manual === true; this.runningScheduleIds.add(schedule.id); const runId = randomUUID(); const runningRun: ScheduleRun = { id: runId, - scheduledFor: schedule.nextRunAt ?? now.toISOString(), + scheduledFor: manual ? now.toISOString() : (schedule.nextRunAt ?? now.toISOString()), startedAt: now.toISOString(), endedAt: null, status: "running", @@ -315,6 +334,7 @@ export class ScheduleService { agentId: result.agentId, output: result.output, error: null, + manual, }); } catch (error) { await this.finishRun({ @@ -324,6 +344,7 @@ export class ScheduleService { agentId: null, output: null, error: error instanceof Error ? error.message : String(error), + manual, }); } finally { this.runningScheduleIds.delete(schedule.id); @@ -337,6 +358,7 @@ export class ScheduleService { agentId: string | null; output: string | null; error: string | null; + manual: boolean; }): Promise { const schedule = await this.inspect(params.scheduleId); const now = this.now(); @@ -359,7 +381,9 @@ export class ScheduleService { updatedAt: now.toISOString(), }; - if (shouldCompleteSchedule(updated, now)) { + if (params.manual) { + // Manual one-shot runs do not advance the cadence or recompute completion. + } else if (shouldCompleteSchedule(updated, now)) { updated = completeSchedule(updated, now); } else if (updated.status === "paused") { updated = { diff --git a/packages/server/src/server/schedule/types.ts b/packages/server/src/server/schedule/types.ts index 62668d3bb..11be629ba 100644 --- a/packages/server/src/server/schedule/types.ts +++ b/packages/server/src/server/schedule/types.ts @@ -90,6 +90,7 @@ export interface CreateScheduleInput { target: ScheduleTarget; maxRuns?: number | null; expiresAt?: string | null; + runOnCreate?: boolean | null; } export interface ScheduleExecutionResult { diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index 9c487dd9b..a16b08ff9 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -2130,6 +2130,23 @@ export class Session { return this.handleChatReadRequest(msg); case "chat/wait": return this.handleChatWaitRequest(msg); + case "loop/run": + return this.handleLoopRunRequest(msg); + case "loop/list": + return this.handleLoopListRequest(msg); + case "loop/inspect": + return this.handleLoopInspectRequest(msg); + case "loop/logs": + return this.handleLoopLogsRequest(msg); + case "loop/stop": + return this.handleLoopStopRequest(msg); + default: + return this.dispatchScheduleMessage(msg); + } + } + + private dispatchScheduleMessage(msg: SessionInboundMessage): Promise | undefined { + switch (msg.type) { case "schedule/create": return this.handleScheduleCreateRequest(msg); case "schedule/list": @@ -2144,16 +2161,8 @@ export class Session { return this.handleScheduleResumeRequest(msg); case "schedule/delete": return this.handleScheduleDeleteRequest(msg); - case "loop/run": - return this.handleLoopRunRequest(msg); - case "loop/list": - return this.handleLoopListRequest(msg); - case "loop/inspect": - return this.handleLoopInspectRequest(msg); - case "loop/logs": - return this.handleLoopLogsRequest(msg); - case "loop/stop": - return this.handleLoopStopRequest(msg); + case "schedule/run-once": + return this.handleScheduleRunOnceRequest(msg); default: return undefined; } @@ -8395,7 +8404,8 @@ export class Session { | "schedule/logs" | "schedule/pause" | "schedule/resume" - | "schedule/delete"; + | "schedule/delete" + | "schedule/run-once"; } >, error: unknown, @@ -8428,6 +8438,7 @@ export class Session { target, maxRuns: request.maxRuns, expiresAt: request.expiresAt, + runOnCreate: request.runOnCreate, }); this.emit({ type: "schedule/create/response", @@ -8550,6 +8561,24 @@ export class Session { } } + private async handleScheduleRunOnceRequest( + request: Extract, + ): Promise { + try { + const schedule = await this.scheduleService.runOnce(request.scheduleId); + this.emit({ + type: "schedule/run-once/response", + payload: { + requestId: request.requestId, + schedule, + error: null, + }, + }); + } catch (error) { + this.emitScheduleRpcError(request, error); + } + } + private emitLoopRpcError( request: Extract< SessionInboundMessage, diff --git a/packages/server/src/shared/messages.ts b/packages/server/src/shared/messages.ts index 86f5459c6..b8c720149 100644 --- a/packages/server/src/shared/messages.ts +++ b/packages/server/src/shared/messages.ts @@ -28,6 +28,7 @@ import { SchedulePauseRequestSchema, ScheduleResumeRequestSchema, ScheduleDeleteRequestSchema, + ScheduleRunOnceRequestSchema, ScheduleCreateResponseSchema, ScheduleListResponseSchema, ScheduleInspectResponseSchema, @@ -35,6 +36,7 @@ import { SchedulePauseResponseSchema, ScheduleResumeResponseSchema, ScheduleDeleteResponseSchema, + ScheduleRunOnceResponseSchema, } from "../server/schedule/rpc-schemas.js"; import { LoopRunRequestSchema, @@ -1765,6 +1767,7 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [ SchedulePauseRequestSchema, ScheduleResumeRequestSchema, ScheduleDeleteRequestSchema, + ScheduleRunOnceRequestSchema, LoopRunRequestSchema, LoopListRequestSchema, LoopInspectRequestSchema, @@ -3378,6 +3381,7 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [ SchedulePauseResponseSchema, ScheduleResumeResponseSchema, ScheduleDeleteResponseSchema, + ScheduleRunOnceResponseSchema, LoopRunResponseSchema, LoopListResponseSchema, LoopInspectResponseSchema, @@ -3483,6 +3487,7 @@ export type ScheduleLogsResponse = z.infer; export type SchedulePauseResponse = z.infer; export type ScheduleResumeResponse = z.infer; export type ScheduleDeleteResponse = z.infer; +export type ScheduleRunOnceResponse = z.infer; export type LoopRunResponse = z.infer; export type LoopListResponse = z.infer; export type LoopInspectResponse = z.infer; @@ -3541,6 +3546,7 @@ export type ScheduleLogsRequest = z.infer; export type SchedulePauseRequest = z.infer; export type ScheduleResumeRequest = z.infer; export type ScheduleDeleteRequest = z.infer; +export type ScheduleRunOnceRequest = z.infer; export type LoopRunRequest = z.infer; export type LoopListRequest = z.infer; export type LoopInspectRequest = z.infer;