fix(cron): keep implicit isolated delivery out of main

This commit is contained in:
Peter Steinberger
2026-05-02 07:41:25 +01:00
parent e7a9623968
commit 741005001b
4 changed files with 75 additions and 7 deletions

View File

@@ -28,6 +28,7 @@ Docs: https://docs.openclaw.ai
- Active Memory: use the configured recall timeout as the blocking prompt-build hook budget by default and move cold-start setup grace behind explicit `setupGraceTimeoutMs` config, so the plugin no longer silently extends 15000 ms configs to 45000 ms on the main lane. Fixes #75843. Thanks @vishutdhar.
- Plugins/web-provider: reuse the active gateway plugin registry for runtime web provider resolution after deriving the same candidate plugin ids as the loader path, avoiding a redundant `loadOpenClawPlugins` call on every request while preserving origin and scope filters. Fixes #75513. Thanks @jochen.
- Crestodian/CLI: exit non-zero when interactive Crestodian is invoked without a TTY, so scripts and CI no longer treat the setup error as success. Fixes #73646 and supersedes #73928 and #74059. Thanks @bittoby, @luyao618, and @Linux2010.
- Cron: keep implicit/default isolated cron announce deliveries out of the main session awareness queue, so isolated jobs do not accumulate in the main conversation. Fixes #61426. Thanks @Lihannon.
- Agents/sandbox: preserve existing workspace file modes when sandbox edits atomically replace files, so 0644 files do not collapse to 0600 after Write/Edit/apply_patch. Fixes #44077. Thanks @patosullivan.
- Agents/models: keep legacy CLI runtime model refs such as `claude-cli/*` in the configured allowlist after canonical runtime migration, so cron `payload.model` overrides keep working. Fixes #75753. Thanks @RyanSandoval.
- Codex/app-server: restart the shared Codex app-server client once when it closes during startup thread resume, preserving the existing thread binding instead of retrying `thread/start` on a closed client. Thanks @vincentkoc.

View File

@@ -104,4 +104,35 @@ describe("runCronIsolatedAgentTurn cron delivery awareness", () => {
expect(peekSystemEvents("global")).toEqual(["global cron digest"]);
});
});
it("does not queue main-session awareness for implicit last-target delivery", async () => {
await withTempCronHome(async (home) => {
const storePath = await writeDefaultAgentSessionStoreEntries({
"agent:main:main": {
sessionId: "main-session",
updatedAt: Date.now(),
lastProvider: "telegram",
lastChannel: "telegram",
lastTo: "123",
},
});
const deps = createCliDeps();
mockAgentPayloads([{ text: "implicit cron digest" }]);
const result = await runAnnounceTurn({
home,
storePath,
sessionKey: "cron:job-1",
deps,
delivery: {
mode: "announce",
channel: "last",
},
});
expect(result.status).toBe("ok");
expect(result.delivered).toBe(true);
expect(peekSystemEvents("agent:main:main")).toEqual([]);
});
});
});

View File

@@ -135,8 +135,12 @@ function makeBaseParams(overrides: {
sessionTarget?: string;
deliveryBestEffort?: boolean;
runSessionKey?: string;
resolvedDeliveryMode?: "explicit" | "implicit";
}): Parameters<typeof dispatchCronDelivery>[0] {
const resolvedDelivery = makeResolvedDelivery();
const resolvedDelivery = {
...makeResolvedDelivery(),
mode: overrides.resolvedDeliveryMode ?? "explicit",
} satisfies Extract<DeliveryTargetResolution, { ok: true }>;
const runStartedAt = overrides.runStartedAt ?? Date.now();
return {
cfg: {} as never,
@@ -422,7 +426,7 @@ describe("dispatchCronDelivery — double-announce guard", () => {
);
});
it("queues main-session awareness for isolated cron jobs after delivery", async () => {
it("queues main-session awareness for isolated cron jobs with explicit delivery targets", async () => {
vi.mocked(countActiveDescendantRuns).mockReturnValue(0);
vi.mocked(isLikelyInterimCronMessage).mockReturnValue(false);
@@ -443,6 +447,23 @@ describe("dispatchCronDelivery — double-announce guard", () => {
});
});
it("skips main-session awareness for isolated cron jobs with implicit delivery targets", async () => {
vi.mocked(countActiveDescendantRuns).mockReturnValue(0);
vi.mocked(isLikelyInterimCronMessage).mockReturnValue(false);
const params = makeBaseParams({
synthesizedText: "Implicit cron update.",
resolvedDeliveryMode: "implicit",
});
const state = await dispatchCronDelivery(params);
expect(state.result).toBeUndefined();
expect(state.delivered).toBe(true);
expect(state.deliveryAttempted).toBe(true);
expect(deliverOutboundPayloads).toHaveBeenCalledTimes(1);
expect(enqueueSystemEvent).not.toHaveBeenCalled();
});
it("skips awareness text when direct delivery strips a silent caption", async () => {
vi.mocked(countActiveDescendantRuns).mockReturnValue(0);
vi.mocked(isLikelyInterimCronMessage).mockReturnValue(false);

View File

@@ -361,10 +361,18 @@ function buildDirectCronDeliveryIdempotencyKey(params: {
return `cron-direct-delivery:v1:${executionId}:${params.delivery.channel}:${accountId}:${normalizedTo}:${threadId}`;
}
function shouldQueueCronAwareness(job: CronJob, deliveryBestEffort: boolean): boolean {
// Keep issue #52136 scoped to isolated runs. Session-bound cron jobs keep
// their existing behavior, and best-effort sends may only partially deliver.
return job.sessionTarget === "isolated" && !deliveryBestEffort;
function shouldQueueCronAwareness(params: {
job: CronJob;
delivery: SuccessfulDeliveryTarget;
deliveryBestEffort: boolean;
}): boolean {
// Keep issue #52136 scoped to isolated runs with an explicit delivery target.
// Default isolated announce delivery must not mirror text into the main session.
return (
params.job.sessionTarget === "isolated" &&
!params.deliveryBestEffort &&
params.delivery.mode === "explicit"
);
}
function resolveCronAwarenessMainSessionKey(params: {
@@ -688,7 +696,14 @@ export async function dispatchCronDelivery(
// Intentionally leave partial success uncached: replay may duplicate the
// successful subset, but caching it here would permanently drop the
// failed payloads by converting the replay into delivered=true.
if (delivered && shouldQueueCronAwareness(params.job, params.deliveryBestEffort)) {
if (
delivered &&
shouldQueueCronAwareness({
job: params.job,
delivery,
deliveryBestEffort: params.deliveryBestEffort,
})
) {
await queueCronAwarenessSystemEvent({
cfg: params.cfgWithAgentDefaults,
jobId: params.job.id,