fix: tighten silent cron exec notifications

This commit is contained in:
Peter Steinberger
2026-04-25 07:09:01 +01:00
parent bd60df3e53
commit 36eae5a2c7
4 changed files with 32 additions and 9 deletions

View File

@@ -96,6 +96,7 @@ Docs: https://docs.openclaw.ai
- Browser/startup: deduplicate concurrent lazy-start calls per profile so simultaneous browser tool requests no longer race into duplicate Chrome launches and `PortInUseError`. (#61772) Thanks @sukhdeepjohar.
- Browser/profiles: recover from stale Chromium `Singleton*` profile locks after crashes or host moves by clearing dead/foreign locks and retrying launch once. Thanks @seanc-dev.
- Browser/existing-session: keep Chrome MCP status probes transport-only and ephemeral, and retry stale cached Playwright attaches once so idle profile checks no longer poison the next real attach. (#57245) Thanks @josephbergvinson.
- Cron/exec: suppress automatic background exec completion wakes only for silent cron jobs with `delivery.mode="none"` while keeping webhook and announce runs observable. (#71391) Thanks @goldmar.
- Reply media: allow sandboxed replies to deliver OpenClaw-managed `media/outbound` and `media/tool-*` attachments without treating them as sandbox escapes, while keeping alias-escape checks on the managed media root. Fixes #71138. Thanks @mayor686, @truffle-dev, and @neeravmakwana.
- CLI/agent: keep `openclaw agent --json` stdout reserved for the JSON response by routing gateway, plugin, and embedded-fallback diagnostics to stderr before execution starts. Fixes #71319.
- Agents/Gemini: retry reasoning-only, empty, and planning-only Gemini turns instead of letting sessions silently stall. Fixes #71074. (#71362) Thanks @neeravmakwana.

View File

@@ -69,7 +69,7 @@ export function createCronPromptExecutor(params: {
thinkLevel: ThinkLevel | undefined;
timeoutMs: number;
messageChannel: string | undefined;
deliveryRequested: boolean;
suppressExecNotifyOnExit: boolean;
resolvedDelivery: {
accountId?: string;
to?: string;
@@ -197,12 +197,12 @@ export function createCronPromptExecutor(params: {
bootstrapContextMode: params.agentPayload?.lightContext ? "lightweight" : undefined,
bootstrapContextRunKind: "cron",
toolsAllow: params.agentPayload?.toolsAllow,
execOverrides: params.deliveryRequested
? undefined
: {
execOverrides: params.suppressExecNotifyOnExit
? {
notifyOnExit: false,
notifyOnExitEmptySuccess: false,
},
}
: undefined,
runId: params.cronSession.sessionEntry.sessionId,
requireExplicitMessageTarget: params.toolPolicy.requireExplicitMessageTarget,
disableMessageTool: params.toolPolicy.disableMessageTool,
@@ -270,7 +270,7 @@ export async function executeCronRun(params: {
isAborted: () => boolean;
thinkLevel: ThinkLevel | undefined;
timeoutMs: number;
deliveryRequested: boolean;
suppressExecNotifyOnExit: boolean;
runStartedAt?: number;
}): Promise<CronExecutionResult> {
const resolvedVerboseLevel: VerboseLevel =
@@ -294,7 +294,7 @@ export async function executeCronRun(params: {
thinkLevel: params.thinkLevel,
timeoutMs: params.timeoutMs,
messageChannel: params.resolvedDelivery.channel,
deliveryRequested: params.deliveryRequested,
suppressExecNotifyOnExit: params.suppressExecNotifyOnExit,
resolvedDelivery: params.resolvedDelivery,
toolPolicy: params.toolPolicy,
skillsSnapshot: params.skillsSnapshot,

View File

@@ -241,7 +241,7 @@ describe("runCronIsolatedAgentTurn message tool policy", () => {
thinkLevel: undefined,
timeoutMs: 60_000,
messageChannel: "messagechat",
deliveryRequested: false,
suppressExecNotifyOnExit: true,
toolPolicy: {
requireExplicitMessageTarget: false,
disableMessageTool: false,
@@ -470,6 +470,26 @@ describe("runCronIsolatedAgentTurn message tool policy", () => {
expect(runEmbeddedPiAgentMock.mock.calls[0]?.[0]?.execOverrides).toBeUndefined();
});
it("keeps automatic exec completion notifications when webhook delivery is active", async () => {
mockRunCronFallbackPassthrough();
resolveCronDeliveryPlanMock.mockReturnValue({
requested: false,
mode: "webhook",
to: "https://example.invalid/cron",
});
await runCronIsolatedAgentTurn({
...makeParams(),
job: makeMessageToolPolicyJob({
mode: "webhook",
to: "https://example.invalid/cron",
}),
});
expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1);
expect(runEmbeddedPiAgentMock.mock.calls[0]?.[0]?.execOverrides).toBeUndefined();
});
it("disables the message tool when webhook delivery is active", async () => {
await expectMessageToolDisabledForPlan({
requested: false,

View File

@@ -408,6 +408,7 @@ type PreparedCronRunContext = {
deliveryPlan: CronDeliveryPlan;
resolvedDelivery: ResolvedCronDeliveryTarget;
deliveryRequested: boolean;
suppressExecNotifyOnExit: boolean;
toolPolicy: ReturnType<typeof resolveCronToolPolicy>;
skillsSnapshot: SkillSnapshot;
liveSelection: CronLiveSelection;
@@ -696,6 +697,7 @@ async function prepareCronRunContext(params: {
deliveryPlan,
resolvedDelivery,
deliveryRequested,
suppressExecNotifyOnExit: deliveryPlan.mode === "none",
toolPolicy,
skillsSnapshot,
liveSelection,
@@ -977,7 +979,7 @@ export async function runCronIsolatedAgentTurn(params: {
isAborted,
thinkLevel: prepared.context.thinkLevel,
timeoutMs: prepared.context.timeoutMs,
deliveryRequested: prepared.context.deliveryRequested,
suppressExecNotifyOnExit: prepared.context.suppressExecNotifyOnExit,
});
if (isAborted()) {
return prepared.context.withRunSession({ status: "error", error: abortReason() });