Skip to content

fix(cron): add per-job allow_silent flag to control [SILENT] suppression - #53917

Open
sergioperezcheco wants to merge 2 commits into
NousResearch:mainfrom
sergioperezcheco:fix/cron-allow-silent-flag-53230
Open

sergioperezcheco wants to merge 2 commits into
NousResearch:mainfrom
sergioperezcheco:fix/cron-allow-silent-flag-53230

Conversation

@sergioperezcheco

Copy link
Copy Markdown
Contributor

Summary

The generic [SILENT] suppression guidance is currently injected into all cron job prompts, including recurring briefing/report jobs that should always send an all-clear. This creates contradictory instructions at runtime:

  • Job-specific instruction: "if everything looks normal, say so explicitly"
  • Scheduler-injected instruction: "if there is genuinely nothing new to report, respond with exactly [SILENT]"

Because both are present, the same recurring report can sometimes deliver an all-clear and sometimes suppress delivery entirely — nondeterministic and trust-undermining for daily briefings.

Root Cause

_build_job_prompt() in cron/scheduler.py unconditionally prepends [SILENT] suppression guidance to every agent-driven cron job prompt (since 89db3aeb2c, 2026-04-05). There was no per-job opt-out mechanism.

Fix

Add a first-class allow_silent boolean field to cron jobs (default True for full backward compatibility):

allow_silent Prompt injection Delivery suppression
True (default) [SILENT] guidance injected [SILENT] responses suppress delivery
False No [SILENT] guidance Agent response always delivered, even if it contains [SILENT]

When allow_silent=False:

  1. _build_job_prompt() omits the SILENT: If there is genuinely nothing new… guidance from the cron hint — but keeps the DELIVERY: instructions (the agent still needs to know not to use send_message).
  2. Delivery path (execute_job): the _is_cron_silence_response() check is gated on job.get("allow_silent", True) — so even if the model returns [SILENT], the response is delivered.

Backward compatibility: jobs created before this change have no allow_silent key. Both code sites use job.get("allow_silent", True) so legacy jobs behave exactly as before.

Usage

# Create a recurring briefing that always delivers
cronjob(
    action="create",
    schedule="0 9 * * *",
    prompt="Give me a daily summary of overnight activity. Always send a brief all-clear if nothing happened.",
    allow_silent=False,
)

# Update an existing job to always deliver
cronjob(action="update", job_id="abc123", allow_silent=False)

Changes

  • cron/jobs.py: allow_silent: bool = True param on create_job(), stored as bool in job dict
  • cron/scheduler.py: conditional [SILENT] hint injection in _build_job_prompt(), gated delivery suppression in execute_job
  • tools/cronjob_tools.py: expose allow_silent in the cronjob tool (create + update actions)
  • tests/tools/test_cron_allow_silent.py: 12 regression tests covering prompt injection, job dict field, back-compat, and silence detection

Test Results

tests/tools/test_cron_allow_silent.py ..... 12 passed
tests/tools/test_cronjob_tools.py ......... 79 passed (no regression)

Closes #53230

@alt-glitch alt-glitch added type/feature New feature or request comp/cron Cron scheduler and job management P2 Medium — degraded but workaround exists labels Jun 28, 2026

@tonydwb tonydwb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

Verdict: Approved

Clean per-job allow_silent flag for controlling [SILENT] suppression in cron jobs (#53230). When allow_silent=False, the suppression guidance is omitted and [SILENT] responses are always delivered — intended for recurring briefing/report jobs that should send an all-clear even when nothing changed.

Key observations:

  • Backward-compatible: jobs created before this field existed default to allow_silent=True via job.get("allow_silent", True).
  • The DELIVERY instruction is always present regardless of allow_silent — only the [SILENT] hint is conditionally injected.
  • The delivery path correctly consults allow_silent before checking _is_cron_silence_response.
  • 117 lines of new tests cover prompt injection, job field storage, back-compat, and silence detection.
  • The cronjob tool properly exposes allow_silent for create and update.

Reviewed by Hermes Agent

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for isolating the conflicting cron instructions; current main still injects the generic hint in cron/scheduler.py:2263-2275 and suppresses recognized successful silence responses in cron/scheduler.py:3501-3509.

Problems

  • The new allow_silent parameter is added to direct cronjob() calls, but the PR does not update CRONJOB_SCHEMA or the registered handler. On current main those live at tools/cronjob_tools.py:970-1090 and :1119-1144, so an agent cannot emit or forward this argument.
  • The new delivery gate also affects internally-created silence. cron/scheduler.py:2574-2598 returns [SILENT] for no_agent empty output and wakeAgent=false; with allow_silent=False, the proposed condition would deliver that marker. tests/cron/test_cron_no_agent.py:213-243 defines both paths as intentionally silent.

Suggested changes

  • Wire the field through the schema and registry handler, with tool-path create/update coverage.
  • Keep internal no-output/wake-gate results silent; apply the opt-out only to an LLM final response. Add run_one_job delivery tests for both paths.

Automated hermes-sweeper review.

Comment thread tools/cronjob_tools.py
@@ -587,6 +587,7 @@ def cronjob(
workdir: Optional[str] = None,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This parameter is not reachable from the registered model tool: this PR does not add allow_silent to CRONJOB_SCHEMA or forward it in registry.register(... handler=...). Please wire both paths and add a tool-path regression test; otherwise the documented cronjob(action=..., allow_silent=False) usage cannot work for an agent.

Comment thread cron/scheduler.py Outdated
@@ -2788,7 +2797,7 @@ def run_one_job(job: dict, *, adapters=None, loop=None, verbose: bool = False) -
# a real report that merely quoted "[SILENT]" mid-sentence (#51438,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This gates every successful SILENT marker, including internal script outcomes. run_job returns SILENT_MARKER for no_agent empty stdout and wakeAgent=false (cron/scheduler.py:2574-2598), whose existing contract is to remain silent. With allow_silent=False, those jobs would deliver literal [SILENT]; preserve internal silence separately and apply this opt-out only to an agent-generated final response.

@teknium1 teknium1 added sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 15, 2026
@sergioperezcheco

Copy link
Copy Markdown
Contributor Author

感谢 review!两个问题都已在 commit 98541aff5 中修复:

1. allow_silent 加入 CRONJOB_SCHEMA

  • 在 tools/cronjob_tools.py 的 CRONJOB_SCHEMA.properties 中新增了 allow_silent(类型 boolean,默认 True),说明文字写清楚了:只控制 agent 生成的 [SILENT] 响应,不影响 no_agent 脚本任务的内部静默逻辑。
  • 在已注册的 handler lambda 中补上了 allow_silent=args.get("allow_silent") 的转发。

2. allow_silent=False 只对 agent 响应生效,不再影响 no_agent 内部静默

  • cron/scheduler.py 中原先的逻辑是 job.get("allow_silent", True) and _is_cron_silence_response(...),这会把 no_agent 脚本任务(空 stdout 或 wakeAgent=false)产生的 SILENT_MARKER 也一起 gate 掉,导致 allow_silent=False 时会把字面量 [SILENT] 当成消息内容发送出去。
  • 改为先判断 _is_cron_silence_response(deliver_content),再在内层用 job.get("no_agent") or job.get("allow_silent", True) 决定是否跳过投递。这样 no_agent 任务的内部静默信号在任何情况下都会保持静默,allow_silent=False 只影响 LLM agent 显式返回的 [SILENT] 响应。
if should_deliver and success and _is_cron_silence_response(deliver_content):
    if job.get("no_agent") or job.get("allow_silent", True):
        logger.info("Job '%s': agent returned %s — skipping delivery", job["id"], SILENT_MARKER)
        should_deliver = False

没有 rebase 到 main,直接在原分支上加了 fixup commit。

@sergioperezcheco

Copy link
Copy Markdown
Contributor Author

Thanks @teknium1 — both points are addressed in 98541aff5 (pushed the day after your review; sorry for not pinging explicitly):

  1. allow_silent wired through the tool path: Added to CRONJOB_SCHEMA properties (boolean, default True) with a description explaining the agent-vs-script distinction, and forwarded in the registry.register(...) handler via allow_silent=args.get("allow_silent").

  2. Internal SILENT_MARKER contract preserved: The opt-out now only fires for AGENT-generated [SILENT] responses. The condition is if job.get("no_agent") or job.get("allow_silent", True) — so no_agent script jobs (empty stdout, wakeAgent=false) remain silent regardless of the flag, and the literal "[SILENT]" string is never delivered as a message.

tests/tools/test_cron_allow_silent.py covers both paths. Happy to adjust if the schema description or condition needs tweaking.

@sergioperezcheco
sergioperezcheco force-pushed the fix/cron-allow-silent-flag-53230 branch from 98541af to abaf218 Compare July 19, 2026 04:38
@sergioperezcheco

Copy link
Copy Markdown
Contributor Author

Rebased onto the latest main (was conflicting on the deferred-agent-teardown refactor in run_one_job). The delivery-path [SILENT] gate now sits inside main's new try/finally block and reads: if job.get("no_agent") or job.get("allow_silent", True) — so internal script silence (empty no_agent output, wakeAgent=false) stays silent unconditionally, and allow_silent=False only forces delivery of an explicit agent [SILENT] response. tests/tools/test_cron_allow_silent.py (12 tests) passes against current main.

…ent responses only

Address review feedback on PR NousResearch#53917:

1. tools/cronjob_tools.py:
   - Add allow_silent (boolean, default True) to CRONJOB_SCHEMA properties
     so the agent can set it via the cronjob() tool.
   - Forward allow_silent in the registered handler lambda.

2. cron/scheduler.py:
   - allow_silent=False now only applies to AGENT-generated [SILENT]
     responses. Internal silences from no_agent script jobs (empty stdout
     or wakeAgent=false) remain silent regardless of the flag, since they
     are scheduler-internal signals, not an agent decision. Previously
     allow_silent=False would deliver the literal '[SILENT]' string as the
     message for a no_agent job.
@sergioperezcheco
sergioperezcheco force-pushed the fix/cron-allow-silent-flag-53230 branch from abaf218 to 9268a55 Compare August 10, 2026 15:28
@dragos-cociu

Copy link
Copy Markdown

Thanks for the review and for pushing the follow-up commits. I pulled the current diff and can confirm that both previously identified gaps appear to be addressed:

  • CRONJOB_SCHEMA now exposes allow_silent, and the registered handler forwards it for both create and update.
  • The delivery gate preserves internal no_agent silence independently of allow_silent, so empty stdout and wakeAgent=false do not leak the literal [SILENT] marker.

I re-checked this against current main: there is still no allow_silent or equivalent per-job silence policy in the upstream codebase. This remains a product-level contract gap, not merely model variability. A recurring briefing that promises an explicit all-clear is still subject to a global scheduler-level [SILENT] policy unless it has an explicit per-job override.

Before merge, I think two additional areas should be considered:

  1. Human-facing configuration paths

    The current changes cover the agent tool schema and internal create/update paths, but do not appear to expose the field through the CLI, REST API, or web/desktop create/edit forms. The primary use case in the issue is a user-configured recurring briefing, so users should be able to inspect and set this policy without relying on an agent-generated tool call.

    Legacy jobs should continue to default to the current behavior (allow_silent=True).

  2. Marker leakage when silence is forbidden

    When allow_silent=False, the delivery gate no longer suppresses an agent-generated [SILENT] response. However, if the model still emits [SILENT], NO_REPLY, or NO REPLY, the raw marker could be delivered verbatim to the user instead of becoming a meaningful all-clear.

    The behavior should be defined and tested explicitly. Possible policies include:

    • replace the marker with a configured/default all-clear;
    • retry or classify it as a policy violation;
    • deliver a concise scheduler-generated all-clear;
    • or reject the configuration/model response combination.

Additional acceptance criteria I would recommend:

  • test actual run_one_job() delivery decisions, not only prompt construction and field storage;
  • cover agent jobs with allow_silent=True and False;
  • cover no_agent empty-output and wake-gate paths;
  • define the behavior for [SILENT]/NO_REPLY when silence is forbidden;
  • document that the option controls agent-generated silence only and never overrides deterministic script silence;
  • expose the setting consistently in create, update, list, CLI, API, and UI paths where applicable.

The exact name (allow_silent versus delivery_mode=conditional|always) is secondary. The important part is an explicit, persisted, inspectable, backward-compatible contract enforced at delivery time rather than delegated to prompt wording.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cron Cron scheduler and job management P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Generic cron [SILENT] prompt policy suppresses recurring briefing/report jobs that should always send an all-clear

5 participants