diff --git a/src/agents/system-prompt.ts b/src/agents/system-prompt.ts index ed97fd539..4de3a0f67 100644 --- a/src/agents/system-prompt.ts +++ b/src/agents/system-prompt.ts @@ -49,9 +49,14 @@ function buildUserIdentitySection(ownerLine: string | undefined, isMinimal: bool return ["## User Identity", ownerLine, ""]; } -function buildTimeSection(params: { userTimezone?: string }) { +function buildTimeSection(params: { userTimezone?: string; userTime?: string }) { if (!params.userTimezone) return []; - return ["## Current Date & Time", `Time zone: ${params.userTimezone}`, ""]; + const lines = ["## Current Date & Time"]; + if (params.userTime) { + lines.push(params.userTime); + } + lines.push(`Time zone: ${params.userTimezone}`, ""); + return lines; } function buildReplyTagsSection(isMinimal: boolean) { @@ -294,6 +299,7 @@ export function buildAgentSystemPrompt(params: { : undefined; const reasoningLevel = params.reasoningLevel ?? "off"; const userTimezone = params.userTimezone?.trim(); + const userTime = params.userTime?.trim(); const skillsPrompt = params.skillsPrompt?.trim(); const heartbeatPrompt = params.heartbeatPrompt?.trim(); const heartbeatPromptLine = heartbeatPrompt @@ -445,6 +451,7 @@ export function buildAgentSystemPrompt(params: { ...buildUserIdentitySection(ownerLine, isMinimal), ...buildTimeSection({ userTimezone, + userTime, }), "## Workspace Files (injected)", "These user-editable files are loaded by Moltbot and included below in Project Context.", diff --git a/src/cron/service/jobs.test.ts b/src/cron/service/jobs.test.ts new file mode 100644 index 000000000..4c165bbb8 --- /dev/null +++ b/src/cron/service/jobs.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, it } from "vitest"; + +import type { CronJob } from "../types.js"; +import { computeJobNextRunAtMs } from "./jobs.js"; + +function makeEveryJob(overrides: Partial = {}): CronJob { + return { + id: "test-job", + enabled: true, + createdAtMs: 1000, + updatedAtMs: 1000, + schedule: { kind: "every", everyMs: 3600_000 }, // 1 hour + sessionTarget: "main", + wakeMode: "next-heartbeat", + payload: { kind: "systemEvent", text: "test" }, + state: {}, + ...overrides, + }; +} + +describe("computeJobNextRunAtMs", () => { + it("anchors to lastRunAtMs for every jobs that have run", () => { + const job = makeEveryJob({ + state: { lastRunAtMs: 1000 }, + }); + const nowMs = 5000; + const next = computeJobNextRunAtMs(job, nowMs); + // Should be lastRunAtMs + everyMs, NOT nowMs + everyMs + expect(next).toBe(1000 + 3600_000); + }); + + it("falls back to anchorMs when no lastRunAtMs", () => { + const job = makeEveryJob({ + schedule: { kind: "every", everyMs: 3600_000, anchorMs: 500 }, + state: {}, // No lastRunAtMs + }); + const next = computeJobNextRunAtMs(job, 1000); + expect(next).toBe(500 + 3600_000); + }); + + it("falls back to nowMs when neither lastRunAtMs nor anchorMs exist", () => { + const job = makeEveryJob({ + schedule: { kind: "every", everyMs: 3600_000 }, // No anchorMs + state: {}, // No lastRunAtMs + }); + const next = computeJobNextRunAtMs(job, 1000); + expect(next).toBe(1000 + 3600_000); + }); + + it("does not affect cron expression schedules", () => { + const job: CronJob = { + id: "test-job", + enabled: true, + createdAtMs: 1000, + updatedAtMs: 1000, + schedule: { kind: "cron", expr: "0 9 * * *", tz: "UTC" }, // Daily at 9am UTC + sessionTarget: "main", + wakeMode: "next-heartbeat", + payload: { kind: "systemEvent", text: "test" }, + state: { lastRunAtMs: 500 }, // Should be ignored for cron expr + }; + const nowMs = Date.parse("2025-12-13T00:00:00.000Z"); + const next = computeJobNextRunAtMs(job, nowMs); + // Should be next 9am UTC, not anchored to lastRunAtMs + expect(next).toBe(Date.parse("2025-12-13T09:00:00.000Z")); + }); + + it("returns undefined for disabled jobs", () => { + const job = makeEveryJob({ enabled: false }); + const next = computeJobNextRunAtMs(job, 1000); + expect(next).toBeUndefined(); + }); +}); diff --git a/src/cron/service/jobs.ts b/src/cron/service/jobs.ts index 132156a0c..4e61228f4 100644 --- a/src/cron/service/jobs.ts +++ b/src/cron/service/jobs.ts @@ -40,6 +40,12 @@ export function computeJobNextRunAtMs(job: CronJob, nowMs: number): number | und if (job.state.lastStatus === "ok" && job.state.lastRunAtMs) return undefined; return job.schedule.atMs; } + // For "every" jobs, anchor to lastRunAtMs to prevent drift on updates/restarts + if (job.schedule.kind === "every") { + const anchor = job.state.lastRunAtMs ?? job.schedule.anchorMs ?? nowMs; + return computeNextRunAtMs({ ...job.schedule, anchorMs: anchor }, nowMs); + } + // "cron" expressions are clock-based return computeNextRunAtMs(job.schedule, nowMs); }