diff --git a/CHANGELOG.md b/CHANGELOG.md index 7615a8f8e82..84c25ab5df9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,7 @@ Docs: https://docs.openclaw.ai - Cron/Schedule: for `every` jobs, prefer `lastRunAtMs + everyMs` when still in the future after restarts, then fall back to anchor scheduling for catch-up windows, so NEXT timing matches the last successful cadence. (#22895) Thanks @SidQin-cyber. - Cron/Service: execute manual `cron.run` jobs outside the cron lock (while still persisting started/finished state atomically) so `cron.list` and `cron.status` remain responsive during long forced runs. (#23628) Thanks @dsgraves. - Cron/Timer: keep a watchdog recheck timer armed while `onTimer` is actively executing so the scheduler continues polling even if a due-run tick stalls for an extended period. (#23628) Thanks @dsgraves. +- Cron/Run log: clean up settled per-path run-log write queue entries so long-running cron uptime does not retain stale promise bookkeeping in memory. - Cron/Isolation: force fresh session IDs for isolated cron runs so `sessionTarget="isolated"` executions never reuse prior run context. (#23470) Thanks @echoVic. - Plugins/Install: strip `workspace:*` devDependency entries from copied plugin manifests before `npm install --omit=dev`, preventing `EUNSUPPORTEDPROTOCOL` install failures for npm-published channel plugins (including Feishu and MS Teams). - Feishu/Plugins: restore bundled Feishu SDK availability for global installs and strip `openclaw: workspace:*` from plugin `devDependencies` during plugin-version sync so npm-installed Feishu plugins do not fail dependency install. (#23611, #23645, #23603) diff --git a/src/cron/run-log.test.ts b/src/cron/run-log.test.ts index 2889688013c..ed77d17a9c4 100644 --- a/src/cron/run-log.test.ts +++ b/src/cron/run-log.test.ts @@ -2,7 +2,12 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { describe, expect, it } from "vitest"; -import { appendCronRunLog, readCronRunLogEntries, resolveCronRunLogPath } from "./run-log.js"; +import { + appendCronRunLog, + getPendingCronRunLogWriteCountForTests, + readCronRunLogEntries, + resolveCronRunLogPath, +} from "./run-log.js"; describe("cron run log", () => { async function withRunLogDir(prefix: string, run: (dir: string) => Promise) { @@ -185,4 +190,18 @@ describe("cron run log", () => { expect(entries[1]?.usage?.input_tokens).toBeUndefined(); }); }); + + it("cleans up pending-write bookkeeping after appends complete", async () => { + await withRunLogDir("openclaw-cron-log-pending-", async (dir) => { + const logPath = path.join(dir, "runs", "job-cleanup.jsonl"); + await appendCronRunLog(logPath, { + ts: 1, + jobId: "job-cleanup", + action: "finished", + status: "ok", + }); + + expect(getPendingCronRunLogWriteCountForTests()).toBe(0); + }); + }); }); diff --git a/src/cron/run-log.ts b/src/cron/run-log.ts index 0fd6de76e94..cdb54addea2 100644 --- a/src/cron/run-log.ts +++ b/src/cron/run-log.ts @@ -27,6 +27,10 @@ export function resolveCronRunLogPath(params: { storePath: string; jobId: string const writesByPath = new Map>(); +export function getPendingCronRunLogWriteCountForTests() { + return writesByPath.size; +} + async function pruneIfNeeded(filePath: string, opts: { maxBytes: number; keepLines: number }) { const stat = await fs.stat(filePath).catch(() => null); if (!stat || stat.size <= opts.maxBytes) { @@ -63,7 +67,13 @@ export async function appendCronRunLog( }); }); writesByPath.set(resolved, next); - await next; + try { + await next; + } finally { + if (writesByPath.get(resolved) === next) { + writesByPath.delete(resolved); + } + } } export async function readCronRunLogEntries(