Skip to content

feat(cron/slack): flat in-channel continuable cron delivery surface - #56254

Merged
teknium1 merged 4 commits into
mainfrom
hermes/hermes-c405d782
Jul 1, 2026
Merged

feat(cron/slack): flat in-channel continuable cron delivery surface#56254
teknium1 merged 4 commits into
mainfrom
hermes/hermes-c405d782

Conversation

@teknium1

@teknium1 teknium1 commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a per-platform cron_continuable_surface config key so a continuable cron job can deliver flat into a Slack channel — no dedicated hidden thread — and still be replied-to and continued in context. Salvage of #56096 by @benbarclay, rebased onto current main.

Today continuable cron (cron.mirror_delivery / per-job attach_to_session) is thread-preferred: on any thread-capable platform it unconditionally mints a hidden handoff thread. This adds a "continuable but flat" mode.

How it works

platforms.<p>.extra.cron_continuable_surface takes "thread" (default, byte-identical to today) or "in_channel". In in_channel mode the scheduler skips the thread-open branch (leaves thread_id=None) so the delivery posts flat, then _seed_cron_channel_session creates the flat shared-channel session (slack, chat_id, None) and mirrors the brief into it — the same bucket reply_in_thread: false routes inbound channel replies to. A plain channel reply resolves to that session with the brief in context.

The scheduler reads the key generically from platform config; the in_channel branch is gated on the base-adapter capability flag supports_inchannel_continuable (Slack = True). Any platform without an implementation fails safe to thread with a debug log — no Slack-only special-case in core.

Changes

  • gateway/platforms/base.pysupports_inchannel_continuable capability flag (default False).
  • plugins/platforms/slack/adapter.py — flag True; _cron_continuable_surface() resolver; connect-time warning when in_channel is set without reply_in_thread: false.
  • gateway/config.py — shared-key bridge line.
  • cron/scheduler.py — resolve the surface generically, gate the in_channel branch on the capability flag, skip thread-open, and _seed_cron_channel_session to create + seed the flat session.
  • Docs: cron.md + slack.md (+ zh-Hans mirrors).

Salvage follow-up (ours)

Corrected stale "no new seed code (G6)" comments in the scheduler and the test class docstring — the earlier "let the existing mirror seed it" design was superseded during implementation (the mirror only appends to an existing session; the flat channel row is absent for a chat_postMessage delivery, so the seed must create it first).

Validation

Result
tests/cron/test_scheduler.py 205 pass
tests/gateway/test_slack_cron_continuable_surface.py 10 pass
tests/gateway/test_slack_mention.py 65 pass
Total 280 pass, 0 fail

Manual offline E2E scripts under tests/manual/ (no test_ funcs, __main__-guarded → not collected by CI). Base 0 commits behind main; diff shows only the PR's 13 files.

Infographic

Flat in-channel cron infographic


Salvaged from #56096. Original contributor @benbarclay's commit authorship preserved via rebase-merge.

Nous Research

benbarclay and others added 4 commits July 1, 2026 02:52
Add a per-platform `cron_continuable_surface` extra key
(`thread` default | `in_channel`) so a continuable cron job can deliver
FLAT into a Slack channel — no dedicated thread — and still be
replied-to. In `in_channel` mode the scheduler skips the thread-open
branch (leaves `thread_id=None`); the shipped origin-mirror then seeds
the `(slack, chat_id, None)` shared-channel session — the same bucket
`reply_in_thread: false` routes inbound channel replies to — so a plain
channel reply continues the job in context.

Design: specs/cron-inchannel-continuable (D1–D7, F5). Model B
(shared-channel session), NOT anchoring to the delivery `ts` — on Slack
replying to a specific message IS threading, so a `ts` anchor would only
relocate the thread, never deliver true threadless continuable.

- gateway/platforms/base.py: `supports_inchannel_continuable` capability
  flag (default False → unsupported platforms fail SAFE to `thread`).
- plugins/platforms/slack/adapter.py: flag=True; `_cron_continuable_surface()`
  resolver (coerces to the two-value enum); `_warn_if_inchannel_without_flat_reply`
  connect-time warning (D5: warn, not hard-require — the misconfig fails safe).
- gateway/config.py: shared-key bridge line (top-level OR nested config).
- cron/scheduler.py: read the key generically from platform config, gate
  the `in_channel` branch on the adapter capability flag, skip thread-open.
  No new seed function (reuses the existing mirror — G6).

Pairing (docs): `in_channel` + `reply_in_thread: false` +
`require_mention: false` (or a free-response channel). Missing
`reply_in_thread: false` fails safe to a threaded continuation.

Gateway-side config flag — `/restart` to apply; NO Slack app reinstall.

Tests (from inside the worktree, PYTHONPATH=$PWD):
- +6 cron scheduler tests (in_channel skips thread-open; seeds flat
  channel session with thread_id=None; thread-mode regression;
  fail-safe on unsupported platform; value coercion). Prove-fail:
  removing the `and not in_channel_surface` guard turns the two
  load-bearing tests RED; restore → GREEN.
- +10 slack resolver/capability/warning tests; +2 config-bridge tests.
- tests/manual/cron_inchannel_e2e.py: offline E2E driving BOTH real
  legs (delivery seed + inbound reply keying) → both converge on
  (slack, C, None).
- No regressions: test_slack.py 216 passed alone; broader sweep green
  (4 pre-existing cross-file-ordering failures reproduce identically on
  pristine origin/main).

Docs: cron.md + slack.md + zh-Hans mirrors of both.
…appends)

Live testing exposed a real bug: an in_channel continuable cron delivered
flat to the channel (✅) but the reply did NOT continue the job — the bot
had no brief in context and confabulated the answer.

Root cause: mirror_to_session only APPENDS to a session that already
exists (_find_session_id → no-op when none matches); it never CREATEs one.
A flat (slack, chat_id, None) row is only created when a human posts a
top-level message the bot processes — a cron chat_postMessage delivery
never goes through the inbound handler, so the row is absent and the brief
is silently dropped. The prior impl relied on the bare mirror (F5/OQ-1
concluded "deletion only" — wrong).

Fix: _seed_cron_channel_session mirrors _seed_cron_thread_session —
get_or_create_session FIRST (chat_type = "dm" if is_dm else "group",
thread_id=None), keyed to the ORIGIN USER'S id, then mirror. The channel
session key embeds user_id (…:group:<chat>:<user>), so a system:cron id
would key the seed away from the reply; the origin user's id makes seed
key == inbound reply key. DM key ignores user_id but needs chat_type=dm
to match the prefix. Wired into the in_channel branch after delivery;
suppresses the generic mirror to avoid double-write.

DM validated (per request): the seeded key equals the inbound DM reply key
for a 1:1 DM; continuation works there too.

Tests:
- Rewrote the in_channel tests to use a real _session_store and the origin
  user_id; assert get_or_create_session is called with the flat, correctly-
  keyed source. Prove-fail: (a) reverting the create step and (b) seeding
  with system:cron each turn a targeted test RED; restore → GREEN.
- +2 direct _seed_cron_channel_session unit tests asserting the KEY-MATCH
  invariant (seed key == inbound reply key) via build_session_key, for both
  channel and DM.
- Rewrote tests/manual/cron_inchannel_e2e.py to drive a REAL SessionStore +
  real mirror_to_session + real _find_session_id + real build_session_key
  (no session-layer mocks — the old mocked E2E is exactly why the bug
  shipped). Asserts the brief lands in the transcript and the reply resolves
  to the same session, for BOTH channel and 1:1 DM.

Full relevant sweep: 283 passed.
Live DM testing showed a reply to a DM cron brief did NOT continue the job.
Root cause: for a 1:1 DM the governing knob is dm_top_level_threads_as_sessions
(default True), NOT reply_in_thread / cron_continuable_surface. Under the
default, each top-level DM keys to a per-message session (…:dm:<chat>:<ts>),
so a reply mints a new ts and can never converge with the flat …:dm:<chat>
session the cron seed creates.

A 1:1 DM has no thread-vs-timeline split, so "in_channel" has no coherent
meaning for a DM — cron_continuable_surface is a channel concept and is a
no-op for DMs. DM continuation is governed entirely by
dm_top_level_threads_as_sessions:
  - false → all top-level DMs share …:dm:<chat> → seed + reply converge → works
  - true (default) → per-message sessions → no continuation (cron or interactive)

Option A (chosen): document the requirement; no code change (the flat-DM seed
from the prior commit already lands correctly when the knob is false). Adds a
":::note 1:1 DMs" admonition to cron.md + the zh-Hans mirror.

Verification (real inbound handler, not a hard-coded assumption — the mistake
that made the earlier DM E2E falsely pass): tests/manual/cron_inchannel_dm_e2e.py
drives the REAL _handle_slack_message for a top-level DM under both knob values
and asserts false→converges (…:dm:D_TESTDM == seed), true→diverges
(…:dm:D_TESTDM:<ts>). See decisions.md D9.
The in_channel surface DOES add a seed: _seed_cron_channel_session CREATES
the flat (platform, chat_id, None) session and mirrors the brief into it,
because mirror_to_session only APPENDS to an existing session and the flat
channel row is otherwise absent for a chat_postMessage delivery. Correct
the scheduler thread-skip comment and the test class docstring, which still
described the earlier 'let the existing mirror seed it' design.
@alt-glitch alt-glitch added type/feature New feature or request comp/cron Cron scheduler and job management platform/slack Slack app adapter sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages P3 Low — cosmetic, nice to have labels Jul 1, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Salvage of #56096. Complements #44537 (threaded cron delivery) in the cron-delivery-surface cluster — an opt-in flat in-channel mode. related_to. Not a duplicate.

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 P3 Low — cosmetic, nice to have platform/slack Slack app adapter 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.

3 participants