Skip to content

Commit 41a84ea

Browse files
author
OpenClaw (block)
committed
Fix Ourmoji scheduled run activation
1 parent b028d94 commit 41a84ea

3 files changed

Lines changed: 76 additions & 7 deletions

File tree

app/server/plugins/ourmoji-scheduler.ts

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,30 +4,82 @@
44
* On startup runs one catch-up sweep, then sweeps every 15 minutes.
55
*
66
* Sweep algorithm:
7-
* 1. Load all `active` runs.
8-
* 2. For each, compute today's "night date" in the run's earliest
7+
* 1. Reconcile run lifecycle state (`scheduled -> active`,
8+
* `active|paused -> completed`) based on each run's local date.
9+
* 2. Load all `active` runs.
10+
* 3. For each, compute today's "night date" in the run's earliest
911
* participant timezone, and the 21:00 anchor in UTC.
10-
* 3. If the anchor has passed (or we're catching up after downtime),
12+
* 4. If the anchor has passed (or we're catching up after downtime),
1113
* generate assignments for tonight via the assignments service —
1214
* this is idempotent at the DB level.
13-
* 4. Dispatch notifications for each freshly-inserted assignment.
15+
* 5. Dispatch notifications for each freshly-inserted assignment.
1416
*
1517
* Failures are logged but never thrown — the timer keeps ticking.
1618
*/
1719

1820
import { ourmojiSchedulerLogger as logger } from "~/server/services/ourmoji/logger";
19-
import { listExperimentRunsByStatus, getParticipantsForRun } from "~/server/services/ourmoji/repository";
21+
import {
22+
listExperimentRunsByStatus,
23+
getParticipantsForRun,
24+
updateExperimentRunStatus,
25+
} from "~/server/services/ourmoji/repository";
2026
import { generateAssignmentsForNight } from "~/server/services/ourmoji/assignments";
2127
import { dispatchAssignmentNotification } from "~/server/services/ourmoji/notifications";
2228
import {
2329
computeNextAnchorUtc,
30+
localDateInTimezone,
2431
nightDateForAnchor,
2532
} from "~/server/services/ourmoji/schedule";
2633

2734
const SWEEP_INTERVAL_MS = 15 * 60 * 1000; // 15 minutes
2835

36+
async function reconcileRunStatuses(now: Date): Promise<void> {
37+
const scheduledRuns = await listExperimentRunsByStatus(["scheduled"]);
38+
for (const run of scheduledRuns) {
39+
const localToday = localDateInTimezone(
40+
now,
41+
run.earliestParticipantTimezone || "UTC",
42+
);
43+
44+
if (run.endDate < localToday) {
45+
await updateExperimentRunStatus(run.id, "completed");
46+
logger.info("Completed expired scheduled run", {
47+
runId: run.id,
48+
localToday,
49+
});
50+
continue;
51+
}
52+
53+
if (run.startDate <= localToday) {
54+
await updateExperimentRunStatus(run.id, "active");
55+
logger.info("Activated scheduled run", {
56+
runId: run.id,
57+
localToday,
58+
});
59+
}
60+
}
61+
62+
const liveRuns = await listExperimentRunsByStatus(["active", "paused"]);
63+
for (const run of liveRuns) {
64+
const localToday = localDateInTimezone(
65+
now,
66+
run.earliestParticipantTimezone || "UTC",
67+
);
68+
if (run.endDate < localToday) {
69+
await updateExperimentRunStatus(run.id, "completed");
70+
logger.info("Completed finished run", {
71+
runId: run.id,
72+
previousStatus: run.status,
73+
localToday,
74+
});
75+
}
76+
}
77+
}
78+
2979
async function sweep(): Promise<void> {
3080
const now = new Date();
81+
await reconcileRunStatuses(now);
82+
3183
const runs = await listExperimentRunsByStatus(["active"]);
3284
if (runs.length === 0) {
3385
logger.debug("Sweep tick — no active runs");

app/server/services/ourmoji/schedule.test.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import { describe, expect, it } from "vitest";
1010
import {
1111
computeNextAnchorUtc,
12+
localDateInTimezone,
1213
nightDateForAnchor,
1314
nightIndexForRun,
1415
} from "./schedule";
@@ -26,7 +27,7 @@ describe("nightIndexForRun", () => {
2627
});
2728
});
2829

29-
describe("nightDateForAnchor", () => {
30+
describe("localDateInTimezone / nightDateForAnchor", () => {
3031
it("returns the local YYYY-MM-DD for an anchor instant", () => {
3132
// 21:00 UTC on 2026-04-10 — same calendar day in UTC
3233
const anchor = new Date("2026-04-10T21:00:00Z");
@@ -37,6 +38,12 @@ describe("nightDateForAnchor", () => {
3738
const anchor = new Date("2026-04-11T04:00:00Z");
3839
expect(nightDateForAnchor(anchor, "America/Los_Angeles")).toBe("2026-04-10");
3940
});
41+
it("returns today's local date for arbitrary instants", () => {
42+
const instant = new Date("2026-04-29T22:30:00Z");
43+
expect(localDateInTimezone(instant, "UTC")).toBe("2026-04-29");
44+
expect(localDateInTimezone(instant, "Europe/London")).toBe("2026-04-29");
45+
expect(localDateInTimezone(instant, "America/Los_Angeles")).toBe("2026-04-29");
46+
});
4047
});
4148

4249
describe("computeNextAnchorUtc", () => {

app/server/services/ourmoji/schedule.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,17 @@ export function nightDateForAnchor(
5858
anchorUtc: Date,
5959
timezone: string,
6060
): string {
61-
const { year, month, day } = getLocalYMD(anchorUtc, timezone);
61+
return localDateInTimezone(anchorUtc, timezone);
62+
}
63+
64+
/**
65+
* Returns the local YYYY-MM-DD for an arbitrary instant in `timezone`.
66+
*/
67+
export function localDateInTimezone(
68+
instant: Date,
69+
timezone: string,
70+
): string {
71+
const { year, month, day } = getLocalYMD(instant, timezone);
6272
return `${pad4(year)}-${pad2(month)}-${pad2(day)}`;
6373
}
6474

0 commit comments

Comments
 (0)