fix(schedule): keep firing after a run that could not be recorded (#6334)

* fix(schedule): keep firing after a run that could not be recorded

A schedule whose running-run write failed stayed marked as running in
memory, so every later tick skipped it and run-once reported it as
already running until the daemon restarted. The running mark is now
cleared whenever the run ends, including when it could not be recorded,
and the next tick retries the due run.

* test(schedule): skip the unwritable-directory test as root

---------

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
This commit is contained in:
paseo-bot[bot]
2026-10-07 22:55:19 +02:00
committed by GitHub
parent 2d55870490
commit 0fc6bf0c94
2 changed files with 74 additions and 34 deletions
@@ -1,4 +1,4 @@
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { chmod, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
@@ -2867,6 +2867,44 @@ describe("ScheduleService", () => {
expect(inspected.runs[0]?.error).toBe("network blip");
});
// chmod cannot make a directory unwritable on Windows or for root.
test.skipIf(process.platform === "win32" || process.getuid?.() === 0)(
"fires again after a tick whose run could not be recorded",
async () => {
const service = createScheduleService({
paseoHome: tempDir,
logger: createTestLogger(),
agentManager: new AgentManager({ logger: createTestLogger() }),
agentStorage,
providerSnapshotManager: NO_UNATTENDED_SCHEDULE_POLICY,
now: () => now,
runner: async () => ({ agentId: null, output: "ran" }),
});
const created = await service.create({
prompt: "ping target",
cadence: { type: "every", everyMs: 60_000 },
target: { type: "agent", agentId: "88888888-8888-4888-8888-888888888888" },
});
const schedulesDir = join(tempDir, "schedules");
now = new Date("2026-01-01T00:01:00.000Z");
await chmod(schedulesDir, 0o555);
try {
await expect(service.tick()).rejects.toThrow();
} finally {
await chmod(schedulesDir, 0o755);
}
await service.tick();
const inspected = await service.inspect(created.id);
expect(inspected.runs).toHaveLength(1);
expect(inspected.runs[0]?.status).toBe("succeeded");
expect(inspected.nextRunAt).toBe("2026-01-01T00:02:00.000Z");
},
);
test("completes the schedule when a scheduled run targets an archived agent", async () => {
const service = createScheduleService({
paseoHome: tempDir,
@@ -694,6 +694,7 @@ export class ScheduleService {
): Promise<void> {
const manual = options?.manual === true;
this.runningScheduleIds.add(schedule.id);
try {
const runId = randomUUID();
const runningRun: ScheduleRun = {
id: runId,
@@ -730,6 +731,7 @@ export class ScheduleService {
targetGone: error instanceof ScheduleTargetGoneError,
manual,
});
}
} finally {
this.runningScheduleIds.delete(schedule.id);
}