Skip to content

feat(adapter): polymorphic cron-delivery metadata hook - #29291

Closed
deestax wants to merge 1 commit into
NousResearch:mainfrom
T3-Venture-Labs-Limited:feat/cron-polymorphic-delivery-metadata
Closed

feat(adapter): polymorphic cron-delivery metadata hook#29291
deestax wants to merge 1 commit into
NousResearch:mainfrom
T3-Venture-Labs-Limited:feat/cron-polymorphic-delivery-metadata

Conversation

@deestax

@deestax deestax commented May 20, 2026

Copy link
Copy Markdown

What does this PR do?

Adds an optional polymorphic hook BasePlatformAdapter.build_delivery_metadata(...) so platform adapters can enrich the metadata kwarg passed to adapter.send() during cron delivery, without modifying cron/scheduler.py. The default implementation is a no-op (returns base_metadata unchanged, or None when None), so every existing adapter behaves identically.

Mirrors the existing optional-method pattern on BasePlatformAdapter (on_processing_start, on_processing_complete). Follows the same shape as PR #5295 (moved hardcoded honcho argparse out of main.py into a generic plugin surface): platform-specific logic moves out of core into adapter overrides.

Related Issue

No related issue filed yet. Happy to open one first if preferred — design questions for maintainers are inline at the bottom of this description.

Fixes #

Type of Change

  • ✨ New feature (non-breaking change that adds functionality)

Changes Made

  • gateway/platforms/base.py — adds BasePlatformAdapter.build_delivery_metadata(job, status_hint='ok', base_metadata=None) -> Optional[Dict[str, Any]]. Default returns dict(base_metadata) if not None, else None. (+39 lines)
  • cron/scheduler.py_deliver_result calls the hook on the resolved runtime adapter; adds status_hint parameter (default 'ok', set to 'error' by tick() on failed runs); hook call is exception-guarded with a fallback to base_metadata. (+37 lines, -3 lines)
  • tests/gateway/test_build_delivery_metadata.py — 5 new unit tests for the default hook (no-op behavior, None when base_metadata is None, copy semantics, signature stability, status_hint agnosticism). (+98 lines, new file)
  • tests/cron/test_scheduler.py — 3 new integration tests for the scheduler hook path (live-adapter override is called and its return value reaches adapter.send; exception in the hook falls back to base_metadata; tick() forwards status_hint='error' on failed runs). 5 existing tests gain a one-line mock of the new method on their AsyncMock/MagicMock adapters. (+158 lines)
  • website/docs/developer-guide/adding-platform-adapters.md — new "Enriching delivery metadata" subsection under "Cron Delivery" with override template, behavior contract, and a note that the hook fires only on the live-adapter path. (+27 lines)
  • website/docs/developer-guide/cron-internals.md — new "Adapter Metadata Enrichment" subsection under "Delivery Model" with a cross-reference to the adapter-author guide. (+8 lines)

Total: 6 files, +364 / -3 lines.

Motivation

cron/scheduler.py:_deliver_result currently constructs send_metadata as a plain {"thread_id": ...} dict (or None when no thread is set) before calling adapter.send(..., metadata=send_metadata). Adapters that need richer delivery context have no clean override point — the only path today is to fork the scheduler.

Concrete adapter classes that would benefit:

  • Webhook-fallback adapters that POST to an HTTP endpoint when no live gateway connection is available — typically need job_id, job_name, status, ran_at to reconstruct delivery context for the receiver.
  • Audit-logging adapters that need trace_id, user_id, severity for structured logging requirements.
  • Multi-tenant adapters (CRM bridges, issue trackers, helpdesk integrations) that need tenant_id, account_id, project_id, lead_id for routing.
  • Encrypted-channel adapters that need encryption context, key IDs, signing tokens, idempotency keys.
  • Retry-aware adapters that need attempt count, dead-letter queue markers, retry-after deadlines.

In-tree adapters don't need this today (so this PR is strictly additive with zero behavior change), but several future features would become one-method overrides on the adapter rather than scheduler-touching PRs:

  • Telegram cron-into-topic-threads via message_thread_id
  • Discord cron-into-forum-channels via applied_tags; or cron-into-thread via thread_id
  • Slack cron-into-thread via thread_ts; @-mention support via user_id
  • Matrix E2EE flags; m.in_reply_to for reply chaining
  • Email In-Reply-To / References headers for threading; MIME priority
  • WhatsApp template-message metadata for compliance-gated channels
  • Home Assistant action-button payloads; voice-assistant routing
  • SMS delivery-receipt requests; sender-ID overrides

None of those are in scope here; listed as evidence the missing extension point is broadly applicable.

Scope clarification

The hook fires only on the in-gateway live-runtime-adapter delivery path — when cron.scheduler._deliver_result resolves a runtime_adapter from the adapters dict and the gateway's event loop is running. The out-of-process standalone_sender_fn path (used when hermes cron run is invoked outside the gateway process) continues to use the existing keyword-argument shape and bypasses the hook. This is documented in both updated guides.

If an adapter needs metadata enrichment in both paths, the override logic should be mirrored inside its standalone_sender_fn.

How to Test

bash scripts/run_tests.sh tests/gateway/test_build_delivery_metadata.py tests/cron/test_scheduler.py

Expected: 135 tests pass.

Manual verification on shipped adapters: cron deliveries continue to produce the same metadata payload as before this PR. No behavior change for Telegram, Discord, Slack, WhatsApp, Signal, Matrix, Mattermost, Email, SMS, Home Assistant, DingTalk, Feishu, WeCom, Weixin, BlueBubbles, or QQ — they all inherit the no-op default and produce a byte-identical send_metadata value (None when no thread is configured, {"thread_id": X} when one is). Only adapters that explicitly override build_delivery_metadata see different behavior.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (feat(adapter): ...)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this feature (no unrelated commits)
  • I've run bash scripts/run_tests.sh (focused suite — tests/gateway/test_build_delivery_metadata.py + tests/cron/test_scheduler.py) and all 135 tests pass
  • I've added tests for my changes (5 unit + 3 integration)
  • I've tested on my platform: macOS 15 (darwin/arm64)

Documentation & Housekeeping

  • I've updated relevant documentation — adding-platform-adapters.md and cron-internals.md
  • I've updated cli-config.yaml.example if I added/changed config keys — N/A (no config keys added)
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — N/A
  • I've considered cross-platform impact (Windows, macOS) — pure-Python server-side, no OS-specific code
  • I've updated tool descriptions/schemas if I changed tool behavior — N/A (no tool behavior changed)

Design questions for maintainers

I'd appreciate feedback on a few open shape questions before assuming this is final:

  1. Method namebuild_delivery_metadata vs enrich_delivery_metadata vs metadata_for_delivery? Slight preference for build_ since the default constructs (a copy of) the dict from base_metadata, but happy to rename.

  2. Sync vs async — neighboring on_processing_start / on_processing_complete are async. This hook is sync because the cron scheduler dispatch path calling it (_deliver_result) is synchronous. Happy to make it async def for consistency if you prefer — the call site would need a small bridge to asyncio.run_coroutine_threadsafe similar to safe_schedule_threadsafe usage elsewhere in the file.

  3. status_hint parameter — currently (self, job, status_hint='ok', base_metadata=None). Open to dropping it if you'd rather adapters derive status from the job dict, or to making it a richer enum.

  4. Adapter method vs generic plugin hook — could alternatively be a register_hook("cron_build_delivery_metadata", ...)-style observer. Adapter method seems right for per-platform customization (the adapter owns its delivery shape); a generic hook seems right for cross-cutting observers. Open to the alternative if you prefer.

Screenshots / Logs

scripts/run_tests.sh tests/gateway/test_build_delivery_metadata.py tests/cron/test_scheduler.py final summary:

========================= 135 passed in 3.31s =========================

Adds BasePlatformAdapter.build_delivery_metadata(job, status_hint,
base_metadata) so adapters can enrich the metadata passed to
adapter.send() during cron delivery without patching the scheduler.
cron/scheduler.py:_deliver_result calls the hook on the resolved
runtime adapter instead of constructing send_metadata inline.

Mirrors the existing on_processing_start / on_processing_complete
optional-method pattern on BasePlatformAdapter. Same shape as NousResearch#5295
(removed hardcoded honcho argparse from main.py): platform-specific
logic moves out of core into adapter overrides.

The default returns base_metadata unchanged (or None when None), so
all shipped adapters remain byte-identical. The hook fires only on
the live-runtime-adapter delivery path; standalone_sender_fn is
unchanged. Hook exceptions are caught and fall back to base_metadata,
matching _run_processing_hook's pattern.

Tests: 5 unit tests for the default hook + 3 integration tests for
the scheduler path (hook called and result reaches send; exception
fallback; tick() forwards status_hint='error'). Focused suite 135/135.

Docs: adding-platform-adapters.md gains "Enriching delivery metadata"
with an override template; cron-internals.md gains "Adapter Metadata
Enrichment" with a cross-reference.
@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/cron Cron scheduler and job management comp/gateway Gateway runner, session dispatch, delivery labels May 20, 2026
@deestax

deestax commented May 20, 2026

Copy link
Copy Markdown
Author

Closing in favor of #26012 (which I missed during my pre-PR search — apologies). After a careful read of Brian's approach + the underlying issue #26004, his structured-side-channel via metadata["cron"] is the better fit for what was requested:

  • Directly answers [Feature]: Pass structured cron metadata (job_id, response_id) to BasePlatformAdapter.send via metadata= #26004's job_id / job_name / origin requirements with a fixed-key contract that adapter authors can rely on without per-adapter boilerplate.
  • Centralizes the "what fields are exposed" decision in the scheduler — one place to extend when Step 2 (response_id) lands.
  • Smaller surface area, no new abstraction on BasePlatformAdapter to validate.
  • Adapters that ignore unknown metadata keys are unaffected today; ones that want the cron context get it for free with a 3-line read.

This PR's polymorphic-hook design is more general but the generality isn't asked for by #26004, and pushing it now would compete with a PR that's already on the right track. Heading over to #26012 with a couple of extension suggestions (status field, ran_at) that the failure-route and audit-logging use cases would benefit from — if those are out of scope for the minimal patch I'm happy to defer them.

Thanks to @briandevans for #26012 and to @GiorgioRegni for the original issue.

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 comp/gateway Gateway runner, session dispatch, delivery P3 Low — cosmetic, nice to have type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants