Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1241,8 +1241,15 @@ describe("createDreamTaskExecutor — map-memories disposition", () => {
});

expect(promptCalls).toBe(1);
expect(getTaskScheduleState(db, project, task.task)).toMatchObject({
lastRunAt: startedAt + MAP_BATCH_FLOOR_MS - 60_000,
const scheduleState = getTaskScheduleState(db, project, task.task);
// last_run_at records the START of the last successful run — the
// "changed since" cutoff the scheduler's gates compare against — not
// the completion moment. The banked deadline is therefore later than
// the recorded value, and the recorded value is no earlier than the
// run start (`now`, the clock the scheduler was invoked with).
expect(scheduleState?.lastRunAt).toBeGreaterThanOrEqual(startedAt);
expect(scheduleState?.lastRunAt).toBeLessThan(startedAt + MAP_BATCH_FLOOR_MS - 60_000);
expect(scheduleState).toMatchObject({
lastStatus: "completed",
retryCount: 0,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,31 @@ describe("task-scheduler — runDueTasksForProject", () => {
expect(state?.nextDueAt).toBeGreaterThan(now);
});

it("advances last_run_at from the run START on completion (not completion time)", async () => {
db = freshDb();
seedActiveMemory(db);
const now = Date.now();
const tasks = [cfg("verify", "0 3 * * *")];
planDueTasks(db, PROJECT, tasks, now);
forceDue(db, "verify", now);

// Slow executor: a long run makes the start↔finish gap visible, so the
// assertion can discriminate run-start from run-completion advancement.
const executor = async (): Promise<TaskExecOutcome> => {
await new Promise((r) => setTimeout(r, 200));
return { status: "completed" };
};
const before = Date.now();
await runDueTasksForProject({ db, projectIdentity: PROJECT, tasks, executor, now });
const after = Date.now();
const state = getTaskScheduleState(db, PROJECT, "verify");
expect(state?.lastStatus).toBe("completed");
// last_run_at reflects the run START (~before), not the completion (~after):
// a message landing mid-run is newer than the cutoff and re-triggers next slot.
expect(state?.lastRunAt).toBeGreaterThanOrEqual(before);
expect(state?.lastRunAt).toBeLessThan(after - 150);
});

it("skips a due task whose gate fails (no active memories) and advances it", async () => {
db = freshDb();
// No memories → verify gate fails.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,17 +206,18 @@ function advanceAfterRun(
status: "completed" | "failed" | "skipped",
error: string | null,
schedulePatch?: TaskExecOutcome["schedulePatch"],
startedAt?: number,
): void {
writeTaskScheduleState(db, {
projectPath: projectIdentity,
task: due.config.task,
// last_run_at means "last SUCCESSFUL run" — the cutoff for "changed since"
// gates (maintain-docs). A failed or skipped run did NOT process the
// work, so the cutoff must NOT advance past it (mirrors v1, where
// last_dream_at only advanced when a task succeeded).
// last_run_at = the start of the last SUCCESSFUL run — the cutoff for
// "changed since" gates. A message/compartment that landed DURING the run
// (after the start) is newer than the cutoff, so it re-triggers the gate next
// slot instead of being silently skipped. Failed/skipped runs never advance it.
lastRunAt:
status === "completed"
? finishedAt
? (startedAt ?? finishedAt)
: readLastRunAt(db, projectIdentity, due.config.task),
nextDueAt: nextDueAtMs(due.config.schedule, finishedAt, due.scheduledAt),
schedule: due.config.schedule,
Expand Down Expand Up @@ -369,6 +370,7 @@ async function runDomainGroup(
}

let outcome: TaskExecOutcome;
const startedAt = Date.now();
try {
outcome = await executor(due.config, {
db,
Expand All @@ -391,6 +393,7 @@ async function runDomainGroup(
"completed",
null,
outcome.schedulePatch,
startedAt,
);
cb?.onRan?.(due.config.task, outcome.detail, outcome.backlog);
} else if (outcome.transient) {
Expand Down