Conversation
|
…ries Per-job `channel_summary: true` teaches the job's agent to lead its final response with "[SUMMARY] <1-2 lines>"; at delivery time the summary is lifted (non-destructively — only the marker token is stripped, the body keeps its opening lines) and becomes the continuable thread's channel-root seed message in place of the fixed "Hermes handoff — <name>" label, so busy channels get a real TL;DR with the verbose brief threaded under it. - seed_text rides create_handoff_thread as an optional kwarg (base + slack/ discord/telegram/relay) with a bind-time TypeError retry for out-of-tree adapters; relay carries it as an additive thread_create op field - Slack seed escapes broadcast mentions exactly as format_message; the summary is MEDIA-tag-sanitized before seeding; guardrails (2 lines / 400 chars) fall back to the fixed label - also fixes the cronjob registry handler silently dropping the schema-declared attach_to_session argument (channel_summary depends on it) - docs: cron.md (+zh-Hans mirror), relay connector contract (additive seed_text with mandatory outbound mention hygiene), config_defaults note Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ry on the kwarg signature Review follow-up (NousResearch#91792): - _CRON_SUMMARY_MARKER_RE accepts lightweight decoration (**[SUMMARY]**, > [SUMMARY], *[SUMMARY]:*) so a cosmetic format miss cannot dodge the stray-[SILENT] carve-out and swallow a real report; the trailing decoration class is whitespace-terminated so undecorated summary text is never consumed. Prompt hint now teaches the exact literal. - The await-time TypeError retry in _open_continuable_cron_thread fires only when the error names the seed_text kwarg — a genuine TypeError in a modern adapter propagates instead of being masked (and can no longer re-run an adapter that posted its seed before raising). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
7005ed1 to
1202119
Compare
|
Thanks — two of these were real; both are fixed in the follow-up commit (branch also rebased onto current 1. Await-time 2. Decorated marker missing the lift and the stray- 3. Slack mention inventory — verified at parity, no change. 4. 5. Discord per-call |
Summary
Per-job
channel_summary: trueturns a continuable cron's channel-root line into an agent-written TL;DR of the run — the fixed:thread: Hermes handoff — *<name>*seed becomes "All 3 feeds healthy; ingest lag 2m (normal)." while the verbose brief stays threaded under it. Today a continuable delivery (attach_to_session/cron.mirror_delivery) already threads the brief, but the channel line is a static label carrying no information about the run; verbose recurring jobs make busy channels noisy for everyone who only needs the headline. The job's agent is prompted to lead its final response with[SUMMARY] <1-2 lines>; at delivery time the summary is lifted and ridescreate_handoff_threadas the seed body. Extraction is non-destructive by construction — only the marker token is stripped, the body keeps its opening lines — so every lane where no dedicated thread opens (flat delivery, fan-out targets, pinned origin threads, gateway offline, legacy adapters) delivers the complete text unchanged. No existing issue; motivated by production use (multiple Hermes bots + crons sharing Slack channels).Also fixes a latent bug this feature depends on: the
cronjobregistry handler declaredattach_to_sessioninCRONJOB_SCHEMAbut never read it from the model's arguments, so agent-set values were silently dropped.Changes
cron/scheduler.py—_extract_cron_summary(first-line-anchored[SUMMARY]marker, 2-line/400-char guardrails fall back to the fixed label; non-destructive split);_deliver_resultgates extraction on the per-job flag, MEDIA-sanitizes the lifted summary (the seed rides a rawchat_postMessagethat bypasses the send pipeline where tags are normally extracted — aMEDIA:line must not print a local path at channel root), and passes it asseed_text=to_open_continuable_cron_thread; the thread opener forwardsseed_textwith bind-time AND await-timeTypeErrorretries so out-of-tree adapters predating the kwarg (including*args/**kwargswrappers) still open their thread with the label seed;_cron_silence_suppresses_delivery— shared predicate for the delivery gate and the usage-auditresponse_silentfield: a[SUMMARY]-led response with substantive body is never swallowed by a stray trailing[SILENT](marker presence, not guardrail-valid extraction, so a format miss can't become data loss; content-free[SUMMARY]\n\n[SILENT]stays suppressed); the persisted output doc stores the marker-stripped body socontext_from/continuity chains never re-ingest the control token; flagged jobs get a[CHANNEL SUMMARY: ...]block appended to the cron hint — unflagged jobs' hint is byte-identical.cron/jobs.py—create_job(channel_summary=...), conditional-persist like its prerequisite siblingattach_to_session(absent key = pre-feature behavior byte-for-byte). REST/dashboard/CLI create surfaces deliberately not wired: none of them expose theattach_to_sessionfamily today, and a summary toggle whose prerequisite can't be set there would be worse than none — rationale comment in the code, expose the family together when those surfaces grow.tools/cronjob_tools.py—channel_summaryon thecronjobtool (create/update/schema/_format_job); fixes the registry handler dropping the schema-declaredattach_to_sessionargument (channel_summary depends on it);_format_jobnow surfaces both flags (explicit bools only, absent ≠ false).gateway/platforms/base.py—create_handoff_thread(seed_text=None)on the adapter contract: seed-message-anchored platforms use it as the seed body; platforms whose threads have no seed message ignore it.plugins/platforms/slack/adapter.py— seed body fromseed_text, broadcast mentions escaped exactly asformat_message(<!channel>/<!here>/<!everyone>→ literal) so this rawchat_postMessagelane is never the one place agent text can ping a workspace; fixed-label fallback stays raw (trusted constant).plugins/platforms/discord/adapter.py—seed_texton the seed-message fallback only (directcreate_threadhas no seed message), with a pinned per-sendAllowedMentions(everyone=False, roles=False)because the client-level default is env-overridable and must not govern agent-authored seeds; label sends keep the legacy call untouched.plugins/platforms/telegram/adapter.py— accepts and ignoresseed_textfor signature parity (forum topics have no seed message).gateway/relay/adapter.py—seed_textrides thethread_createop as an additive optional field, capped[:2000]; docstring states the connector-side hygiene requirement.docs/relay-connector-contract.md—seed_text?documented onthread_create: connectors MUST render it through the same outbound broadcast-mention hygiene assendcontent (additive within contract_version 1; connectors predating the field ignore it and the gateway degrades to the label seed).website/docs/user-guide/features/cron.md+ zh-Hans mirror — new "Agent-written channel summary" section under Continuable jobs (same-commit mirror sync per thein_channelprecedent).hermes_cli/config_defaults.py— continuable-jobs config comment mentions the per-job flag.tests/—tests/cron/test_cron_channel_summary.py(new, 48 tests: extractor edge cases incl. guardrails and colon/case variants, seed forwarding + both TypeError retries,_deliver_resultgating + MEDIA strip, persisted-output strip, job-store conditional persist, tool schema/update/list, registry-handler regression both directions, silence predicate + tick-level gate, prompt hint byte-identity); Slack seed tests intest_slack_sdk_response.py(+4 incl. mention-escape regression); relay op-field tests intest_relay_threads.py(+3); Discord fallbackAllowedMentionstests intest_discord_allowed_mentions.py(+2).Design constraints
test_without_the_flag_content_is_byte_identical,test_registry_handler_passes_none_when_flags_omitted, prompt-hint identity test).AllowedMentionspin bring the new raw lane up to parity, not beyond.[SILENT]after a real summary delivers instead of vanishing — a visible stray marker beats invisible loss; the documented "why-quiet note +[SILENT]" suppression pattern is untouched (it never leads with[SUMMARY]).Validation
tests/cron/test_cron_channel_summary.py(new)tests/gateway/test_slack_sdk_response.pytests/gateway/relay/test_relay_threads.pytests/gateway/test_discord_allowed_mentions.pytests/cron/full suitetest_monitor_kind.pyhost-config drift-skip; failure set byte-identical onmain, verified in a cleanmainworktree with the same interpreter)scripts/run_tests.sh)FAILEDids diff empty againstmainon this host (local env: host~/.hermesconfig leakage), 0 introduced by this branchruff check(all changed files)HERMES_HOME(mktemp), realcreate_job→ real_deliver_result→ real_open_continuable_cron_thread→ stub Slack adapter; only config load,DeliveryRouter, and session mirror stubbed. Jobdeliver=origin,attach_to_session=true,channel_summary=true; agent response"[SUMMARY] All 3 feeds healthy; ingest lag 2m (normal).\n\nFull report:\n- feed A: 1,204 items..."→ channel root received exactlyAll 3 feeds healthy; ingest lag 2m (normal)., thread1724300000.000100received the 205-char marker-stripped body opening with the same line.Tested on macOS 15 (Darwin 25.6.0), Python 3.12.14.
Type of Change
attach_to_sessionargument was silently dropped by the tool handler — fixed because this feature depends on it)Checklist
Code
attach_to_sessionhandler fix is a dependency of the feature, called out above)scripts/run_tests.sh— feature suites green; full-suite failures verified pre-existing onmain(table above)Documentation & Housekeeping
website/docs/user-guide/features/cron.md+ zh-Hans mirror,docs/relay-connector-contract.md, tool schema descriptionscli-config.yaml.example— N/A: no new global config key (per-job field set via thecronjobtool; the continuable-jobs family it extends is likewise not in the example config)CONTRIBUTING.md/AGENTS.md— N/A: no architecture or workflow changeCRONJOB_SCHEMA.channel_summary, refinedattach_to_sessioninterplay)Security note
The seed message is a new outbound lane for agent-authored text posted at channel root. Both in-tree raw lanes are brought to send-lane parity in this PR (Slack broadcast-mention escape identical to
format_message; Discord pinnedAllowedMentions), the relay contract now states the connector-side requirement in MUST language, and the lifted summary is MEDIA-tag-sanitized so a tag line can't leak a local filesystem path into the channel. Mention-escape and MEDIA-strip regressions are pinned by tests.🤖 Generated with Claude Code