mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
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 <id>` 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.
This commit is contained in:
@@ -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<SingleResult<ScheduleRow>> {
|
||||
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 {
|
||||
|
||||
@@ -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 <path>", "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 <n>", "Maximum number of runs")
|
||||
.option("--expires-in <duration>", "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("<id>", "Schedule ID"),
|
||||
).action(withOutput(runDeleteCommand));
|
||||
|
||||
addJsonAndDaemonHostOptions(
|
||||
schedule
|
||||
.command("run-once")
|
||||
.description("Manually trigger a single run of a schedule without affecting cadence")
|
||||
.argument("<id>", "Schedule ID"),
|
||||
).action(withOutput(runRunOnceCommand));
|
||||
|
||||
return schedule;
|
||||
}
|
||||
|
||||
33
packages/cli/src/commands/schedule/run-once.ts
Normal file
33
packages/cli/src/commands/schedule/run-once.ts
Normal file
@@ -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<SingleResult<ScheduleRow>> {
|
||||
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(() => {});
|
||||
}
|
||||
}
|
||||
@@ -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"),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<ScheduleCreatePayload>;
|
||||
scheduleList(): Promise<ScheduleListPayload>;
|
||||
@@ -137,5 +144,6 @@ export interface ScheduleDaemonClient {
|
||||
schedulePause(input: { id: string }): Promise<SchedulePausePayload>;
|
||||
scheduleResume(input: { id: string }): Promise<ScheduleResumePayload>;
|
||||
scheduleDelete(input: { id: string }): Promise<ScheduleDeletePayload>;
|
||||
scheduleRunOnce(input: { id: string }): Promise<ScheduleRunOncePayload>;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -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<ScheduleRunOncePayload> {
|
||||
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<LoopRunPayload> {
|
||||
return this.sendCorrelatedSessionRequest({
|
||||
requestId: options.requestId,
|
||||
|
||||
@@ -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(),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<StoredSchedule> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
private async runSchedule(
|
||||
schedule: StoredSchedule,
|
||||
now: Date,
|
||||
options?: { manual?: boolean },
|
||||
): Promise<void> {
|
||||
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<void> {
|
||||
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 = {
|
||||
|
||||
@@ -90,6 +90,7 @@ export interface CreateScheduleInput {
|
||||
target: ScheduleTarget;
|
||||
maxRuns?: number | null;
|
||||
expiresAt?: string | null;
|
||||
runOnCreate?: boolean | null;
|
||||
}
|
||||
|
||||
export interface ScheduleExecutionResult {
|
||||
|
||||
@@ -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<void> | 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<SessionInboundMessage, { type: "schedule/run-once" }>,
|
||||
): Promise<void> {
|
||||
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,
|
||||
|
||||
@@ -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<typeof ScheduleLogsResponseSchema>;
|
||||
export type SchedulePauseResponse = z.infer<typeof SchedulePauseResponseSchema>;
|
||||
export type ScheduleResumeResponse = z.infer<typeof ScheduleResumeResponseSchema>;
|
||||
export type ScheduleDeleteResponse = z.infer<typeof ScheduleDeleteResponseSchema>;
|
||||
export type ScheduleRunOnceResponse = z.infer<typeof ScheduleRunOnceResponseSchema>;
|
||||
export type LoopRunResponse = z.infer<typeof LoopRunResponseSchema>;
|
||||
export type LoopListResponse = z.infer<typeof LoopListResponseSchema>;
|
||||
export type LoopInspectResponse = z.infer<typeof LoopInspectResponseSchema>;
|
||||
@@ -3541,6 +3546,7 @@ export type ScheduleLogsRequest = z.infer<typeof ScheduleLogsRequestSchema>;
|
||||
export type SchedulePauseRequest = z.infer<typeof SchedulePauseRequestSchema>;
|
||||
export type ScheduleResumeRequest = z.infer<typeof ScheduleResumeRequestSchema>;
|
||||
export type ScheduleDeleteRequest = z.infer<typeof ScheduleDeleteRequestSchema>;
|
||||
export type ScheduleRunOnceRequest = z.infer<typeof ScheduleRunOnceRequestSchema>;
|
||||
export type LoopRunRequest = z.infer<typeof LoopRunRequestSchema>;
|
||||
export type LoopListRequest = z.infer<typeof LoopListRequestSchema>;
|
||||
export type LoopInspectRequest = z.infer<typeof LoopInspectRequestSchema>;
|
||||
|
||||
Reference in New Issue
Block a user