From 25e93c4a895ca42b7e2b4d934af902defc3e9501 Mon Sep 17 00:00:00 2001 From: matevip Date: Sat, 2 May 2026 15:41:06 +0800 Subject: [PATCH] feat(cron): suppressAgentReply flag for silent cron jobs --- .../cron/delivery/CronDeliveryListener.java | 13 ++++++ .../vip/mate/cron/model/DeliveryConfig.java | 44 +++++++++++++++++-- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/mateclaw-server/src/main/java/vip/mate/cron/delivery/CronDeliveryListener.java b/mateclaw-server/src/main/java/vip/mate/cron/delivery/CronDeliveryListener.java index 08a0f1f2..2e81b637 100644 --- a/mateclaw-server/src/main/java/vip/mate/cron/delivery/CronDeliveryListener.java +++ b/mateclaw-server/src/main/java/vip/mate/cron/delivery/CronDeliveryListener.java @@ -8,6 +8,7 @@ import org.springframework.stereotype.Component; import org.springframework.transaction.event.TransactionPhase; import org.springframework.transaction.event.TransactionalEventListener; import vip.mate.audit.service.AuditEventService; +import vip.mate.cron.model.DeliveryConfig; import java.util.List; import java.util.Optional; @@ -40,6 +41,18 @@ public class CronDeliveryListener { @Async("cronDeliveryExecutor") @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true) public void onCompleted(CronJobCompletedEvent ev) { + // RFC-03 Lane C1: explicit silent mode short-circuits delivery + // resolution. Tools already executed, the run row is already + // persisted — only the agent's narrative reply is withheld from + // the channel. Status row stays NONE (same as "no strategy + // matched") so dashboards keep one canonical "not delivered" + // bucket. + DeliveryConfig dc = ev.job() != null ? ev.job().getDeliveryConfig() : null; + if (dc != null && dc.isAgentReplySuppressed()) { + log.debug("[CronDelivery] Job {} suppressAgentReply=true; skipping delivery", + ev.job().getId()); + return; + } Optional strategy = deliveries.stream() .filter(d -> d.supports(ev.job())) .findFirst(); diff --git a/mateclaw-server/src/main/java/vip/mate/cron/model/DeliveryConfig.java b/mateclaw-server/src/main/java/vip/mate/cron/model/DeliveryConfig.java index 4fbb7ab8..db1f260f 100644 --- a/mateclaw-server/src/main/java/vip/mate/cron/model/DeliveryConfig.java +++ b/mateclaw-server/src/main/java/vip/mate/cron/model/DeliveryConfig.java @@ -33,30 +33,66 @@ public record DeliveryConfig( *

Nullable for backwards compat with rows written before this * field was added (V62 baseline). */ - @Nullable String userId + @Nullable String userId, + /** + * RFC-03 Lane C1 — when {@code TRUE}, {@code CronDeliveryListener} + * skips strategy resolution entirely and the run completes with + * {@code delivery_status='NONE'}. Tools still execute, the run row + * is still persisted, audit + token-usage all work — only the + * agent's narrative reply is suppressed from the channel. + * + *

Use cases (QwenPaw #2452): noon health-check cron that just + * pokes a database and writes structured output, project-weekly + * report jobs that drop a file into a knowledge base, internal + * pipelines that don't need an IM-visible "I did the thing" + * trailing message. + * + *

{@code Boolean} (not {@code boolean}) so JSON deserialization + * of pre-V75 rows leaves the field {@code null} — the listener + * treats {@code null} and {@code FALSE} identically (deliver as + * usual), preserving every existing job's behavior. + */ + @Nullable Boolean suppressAgentReply ) { /** 3-arg legacy constructor preserved so older deserialized rows still work. */ public DeliveryConfig(@Nullable String targetId, @Nullable String threadId, @Nullable String accountId) { - this(targetId, threadId, accountId, null); + this(targetId, threadId, accountId, null, null); + } + + /** 4-arg legacy constructor — pre-RFC-03 callers that already carry userId. */ + public DeliveryConfig(@Nullable String targetId, + @Nullable String threadId, + @Nullable String accountId, + @Nullable String userId) { + this(targetId, threadId, accountId, userId, null); } /** Convert from the {@link ChannelTarget} carried on a {@code ChatOrigin}. */ public static DeliveryConfig from(@Nullable ChannelTarget t) { if (t == null) return null; - return new DeliveryConfig(t.targetId(), t.threadId(), t.accountId(), null); + return new DeliveryConfig(t.targetId(), t.threadId(), t.accountId(), null, null); } /** Convert from {@link ChannelTarget} + the requester's senderId. */ public static DeliveryConfig from(@Nullable ChannelTarget t, @Nullable String userId) { if (t == null) return null; - return new DeliveryConfig(t.targetId(), t.threadId(), t.accountId(), userId); + return new DeliveryConfig(t.targetId(), t.threadId(), t.accountId(), userId, null); } /** Convert back to a {@link ChannelTarget} for ChatOrigin reconstruction. */ public ChannelTarget toChannelTarget() { return new ChannelTarget(targetId, threadId, accountId); } + + /** + * RFC-03 Lane C1 — convenience predicate so listeners can short-circuit + * delivery resolution without unwrapping the {@code Boolean}. Treats + * {@code null} as {@code false} (the historical default). + */ + public boolean isAgentReplySuppressed() { + return Boolean.TRUE.equals(suppressAgentReply); + } }