Hermes Cron Adds Flat In-Channel Delivery For Slack With Capability-Flag Pattern

Hermes cron jobs that mirror into a chat platform used to land in one place only: a hidden handoff thread. PR #56254, merged today on main, adds a second option for Slack. Set cron_continuable_surface: in_channel and a continuable job posts the brief flat into the channel, no thread. A plain channel reply then continues the job in context via the shared-channel session.
The change is a salvaged branch from @benbarclay's #56096, rebased onto current main and reviewed in. 13 files, +1115/-1, 4 commits, 280 tests passing across the new test_cron_continuable_surface_in_channel class, the Slack pairing tests, and the existing cron scheduler suite.
How the surface works
The scheduler reads a per-platform config key, platforms.<p>.extra.cron_continuable_surface, with two valid values:
"thread"(default) - byte-identical to today's behaviour. The scheduler mints a hidden handoff thread and the brief lands there."in_channel"- the scheduler skips the thread-open branch (leavesthread_id=None) and posts the brief flat into the channel. The continuation surface is the whole-channel session keyed(slack, chat_id, None), the same bucket areply_in_thread: falsereply resolves to.
The misconfig is warned about, not hard-rejected. If you set in_channel without reply_in_thread: false, the Slack adapter logs a connect-time warning. The system still works, the continuation still works, you just do not get the flat delivery surface you asked for.
The capability flag
The interesting design move is the generic seam. The scheduler does not know about Slack specifically. It reads the config key, then gates the in_channel branch on a base-adapter capability flag:
# gateway/platforms/base.py
supports_inchannel_continuable: bool = False
Slack flips this to True because Slack has both halves the surface needs: a flat-reply outbound gate (reply_in_thread: false -> _resolve_thread_ts returns None for top-level channel messages) and a whole-channel inbound session bucket keyed (platform, channel_id, None). Any platform without an implementation fails safe to thread with a debug log. No Slack-only special case in core.
The scheduler reads the flag generically:
getattr(adapter, "supports_inchannel_continuable", False)
That getattr(..., False) default is the fail-safe. A platform that has not opted in cannot accidentally land flat deliveries; unknown is treated as unsupported.
Why a new seed function
The original PR assumed the existing delivery mirror would seed the flat session. It does not. mirror_to_session only appends to a session that already exists (_find_session_id -> no-op when none matches), and a flat channel (slack, channel_id, None) row is only created when a human posts a top-level message the bot processes. A chat_postMessage cron delivery never goes through the inbound handler, so the row is usually absent and the mirror silently drops the brief.
That was verified live before the fix went in: the brief never landed, the reply had no context.
PR #56254 adds _seed_cron_channel_session to mirror _seed_cron_thread_session. It creates the flat (slack, chat_id, None) row first, then mirrors the brief into it. Two commits, both authored by @benbarclay, document the journey from "let the existing mirror seed it" to "we have to create it first." The salvage follow-up by @teknium1 corrects the stale "no new seed code" comments in the scheduler and the test class docstring that survived the rebase.
The file-level shape
| File | Additions | Deletions |
|---|---|---|
| cron/scheduler.py | +162 | -1 |
| tests/cron/test_scheduler.py | +249 | 0 |
| plugins/platforms/slack/adapter.py | +65 | 0 |
| tests/gateway/test_slack_cron_continuable_surface.py | +149 | 0 |
| tests/manual/cron_inchannel_dm_e2e.py | +119 | 0 |
| tests/gateway/test_slack_mention.py | +50 | 0 |
| docs/cron.md + docs/slack.md (+ zh-Hans mirrors) | +292 | 0 |
| gateway/config.py | +2 | 0 |
| gateway/platforms/base.py | +15 | 0 |
| Total | +1115 | -1 |
Roughly 50% of the change is tests. The new TestCronContinuableSurfaceInChannel class is 249 lines covering thread-skip behaviour, session creation, mirror dispatch, and the capability-flag gate. The manual E2E harness in tests/manual/cron_inchannel_dm_e2e.py is __main__-guarded so CI does not collect it, and it documents the DM scoping decision explicitly.
The DM scoping
Option A, documented in the PR body and the manual E2E: cron_continuable_surface is a channel feature. For a 1:1 DM, the governing knob is the pre-existing dm_top_level_threads_as_sessions. A DM has no thread-vs-timeline split, so DM continuation works only when top-level DMs share one flat session (dm_top_level_threads_as_sessions: false). Under the default (true), a top-level DM reply keys to a per-message ...:dm:<chat>:<ts> session that diverges from the flat seed.
The manual E2E proves this against the real inbound handler (SlackAdapter._handle_slack_message), with no hard-coded thread_id assumption. The earlier E2E in the original branch falsely passed because it did not exercise the real session-key resolution path.
Validation summary
| Test file | Passing |
|---|---|
| tests/cron/test_scheduler.py | 205 |
| tests/gateway/test_slack_cron_continuable_surface.py | 10 |
| tests/gateway/test_slack_mention.py | 65 |
| Total | 280 pass, 0 fail |
What this fixes in practice
For cron jobs that deliver into a busy channel, a hidden thread hides the brief from anyone not following the link. in_channel makes the cron job visible in the channel scrollback, with the same continuation surface a reply_in_thread: false reply gives. The pair (cron_continuable_surface: in_channel + reply_in_thread: false) is the configuration.
The fail-safe behaviour is worth noting: a platform without supports_inchannel_continuable = True that sees in_channel in its config logs a debug line and treats it as thread. No cron job silently breaks because someone set the key on a Telegram adapter.
[^1]: @teknium1. "feat(cron/slack): flat in-channel continuable cron delivery surface." Hermes Agent PR #56254. Merged 2026-07-01.
[^2]: @benbarclay. "Original #56096 branch." Hermes Agent. Salvage base for #56254.
[^3]: Hermes Agent. "Cron scheduler." Source. _seed_cron_channel_session lives in cron/scheduler.py.
[^4]: Hermes Agent. "Slack adapter." Source. _cron_continuable_surface and _warn_if_inchannel_without_flat_reply live in the Slack adapter.