Verified: - pnpm install --frozen-lockfile - pnpm check - pnpm test -- --run src/cron/service.failure-alert.test.ts src/cli/cron-cli.test.ts src/gateway/protocol/cron-validators.test.ts Co-authored-by: 0xbrak <181251288+0xbrak@users.noreply.github.com> Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
155 lines
4.5 KiB
TypeScript
155 lines
4.5 KiB
TypeScript
import type { CronConfig } from "../../config/types.cron.js";
|
|
import type { HeartbeatRunResult } from "../../infra/heartbeat-wake.js";
|
|
import type {
|
|
CronDeliveryStatus,
|
|
CronJob,
|
|
CronJobCreate,
|
|
CronJobPatch,
|
|
CronMessageChannel,
|
|
CronRunOutcome,
|
|
CronRunStatus,
|
|
CronRunTelemetry,
|
|
CronStoreFile,
|
|
} from "../types.js";
|
|
|
|
export type CronEvent = {
|
|
jobId: string;
|
|
action: "added" | "updated" | "removed" | "started" | "finished";
|
|
runAtMs?: number;
|
|
durationMs?: number;
|
|
status?: CronRunStatus;
|
|
error?: string;
|
|
summary?: string;
|
|
delivered?: boolean;
|
|
deliveryStatus?: CronDeliveryStatus;
|
|
deliveryError?: string;
|
|
sessionId?: string;
|
|
sessionKey?: string;
|
|
nextRunAtMs?: number;
|
|
} & CronRunTelemetry;
|
|
|
|
export type Logger = {
|
|
debug: (obj: unknown, msg?: string) => void;
|
|
info: (obj: unknown, msg?: string) => void;
|
|
warn: (obj: unknown, msg?: string) => void;
|
|
error: (obj: unknown, msg?: string) => void;
|
|
};
|
|
|
|
export type CronServiceDeps = {
|
|
nowMs?: () => number;
|
|
log: Logger;
|
|
storePath: string;
|
|
cronEnabled: boolean;
|
|
/** CronConfig for session retention settings. */
|
|
cronConfig?: CronConfig;
|
|
/** Default agent id for jobs without an agent id. */
|
|
defaultAgentId?: string;
|
|
/** Resolve session store path for a given agent id. */
|
|
resolveSessionStorePath?: (agentId?: string) => string;
|
|
/** Path to the session store (sessions.json) for reaper use. */
|
|
sessionStorePath?: string;
|
|
enqueueSystemEvent: (
|
|
text: string,
|
|
opts?: { agentId?: string; sessionKey?: string; contextKey?: string },
|
|
) => void;
|
|
requestHeartbeatNow: (opts?: { reason?: string; agentId?: string; sessionKey?: string }) => void;
|
|
runHeartbeatOnce?: (opts?: {
|
|
reason?: string;
|
|
agentId?: string;
|
|
sessionKey?: string;
|
|
/** Optional heartbeat config override (e.g. target: "last" for cron-triggered heartbeats). */
|
|
heartbeat?: { target?: string };
|
|
}) => Promise<HeartbeatRunResult>;
|
|
/**
|
|
* WakeMode=now: max time to wait for runHeartbeatOnce to stop returning
|
|
* { status:"skipped", reason:"requests-in-flight" } before falling back to
|
|
* requestHeartbeatNow.
|
|
*/
|
|
wakeNowHeartbeatBusyMaxWaitMs?: number;
|
|
/** WakeMode=now: delay between runHeartbeatOnce retries while busy. */
|
|
wakeNowHeartbeatBusyRetryDelayMs?: number;
|
|
runIsolatedAgentJob: (params: {
|
|
job: CronJob;
|
|
message: string;
|
|
abortSignal?: AbortSignal;
|
|
}) => Promise<
|
|
{
|
|
summary?: string;
|
|
/** Last non-empty agent text output (not truncated). */
|
|
outputText?: string;
|
|
/**
|
|
* `true` when the isolated run already delivered its output to the target
|
|
* channel (including matching messaging-tool sends). See:
|
|
* https://github.com/openclaw/openclaw/issues/15692
|
|
*/
|
|
delivered?: boolean;
|
|
/**
|
|
* `true` when announce/direct delivery was attempted for this run, even
|
|
* if the final per-message ack status is uncertain.
|
|
*/
|
|
deliveryAttempted?: boolean;
|
|
} & CronRunOutcome &
|
|
CronRunTelemetry
|
|
>;
|
|
sendCronFailureAlert?: (params: {
|
|
job: CronJob;
|
|
text: string;
|
|
channel: CronMessageChannel;
|
|
to?: string;
|
|
}) => Promise<void>;
|
|
onEvent?: (evt: CronEvent) => void;
|
|
};
|
|
|
|
export type CronServiceDepsInternal = Omit<CronServiceDeps, "nowMs"> & {
|
|
nowMs: () => number;
|
|
};
|
|
|
|
export type CronServiceState = {
|
|
deps: CronServiceDepsInternal;
|
|
store: CronStoreFile | null;
|
|
timer: NodeJS.Timeout | null;
|
|
running: boolean;
|
|
op: Promise<unknown>;
|
|
warnedDisabled: boolean;
|
|
storeLoadedAtMs: number | null;
|
|
storeFileMtimeMs: number | null;
|
|
};
|
|
|
|
export function createCronServiceState(deps: CronServiceDeps): CronServiceState {
|
|
return {
|
|
deps: { ...deps, nowMs: deps.nowMs ?? (() => Date.now()) },
|
|
store: null,
|
|
timer: null,
|
|
running: false,
|
|
op: Promise.resolve(),
|
|
warnedDisabled: false,
|
|
storeLoadedAtMs: null,
|
|
storeFileMtimeMs: null,
|
|
};
|
|
}
|
|
|
|
export type CronRunMode = "due" | "force";
|
|
export type CronWakeMode = "now" | "next-heartbeat";
|
|
|
|
export type CronStatusSummary = {
|
|
enabled: boolean;
|
|
storePath: string;
|
|
jobs: number;
|
|
nextWakeAtMs: number | null;
|
|
};
|
|
|
|
export type CronRunResult =
|
|
| { ok: true; ran: true }
|
|
| { ok: true; ran: false; reason: "not-due" }
|
|
| { ok: true; ran: false; reason: "already-running" }
|
|
| { ok: false };
|
|
|
|
export type CronRemoveResult = { ok: true; removed: boolean } | { ok: false; removed: false };
|
|
|
|
export type CronAddResult = CronJob;
|
|
export type CronUpdateResult = CronJob;
|
|
|
|
export type CronListResult = CronJob[];
|
|
export type CronAddInput = CronJobCreate;
|
|
export type CronUpdateInput = CronJobPatch;
|