Skip to content

fix(cron): allow api_server sessions to carry a delivery-platform hint for cron origin (#69304) - #69350

Open
webtecnica wants to merge 2 commits into
NousResearch:mainfrom
webtecnica:fix/69304-cron-deliver
Open

fix(cron): allow api_server sessions to carry a delivery-platform hint for cron origin (#69304)#69350
webtecnica wants to merge 2 commits into
NousResearch:mainfrom
webtecnica:fix/69304-cron-deliver

Conversation

@webtecnica

Copy link
Copy Markdown
Contributor

Problem

When a cron job is created through the api_server platform, its origin is stamped with platform="api_server" (from _origin_from_envHERMES_SESSION_PLATFORM). At fire time, deliver=origin resolves to the api_server adapter which has no send(), so the delivery silently fails with:

API server uses HTTP request/response, not send()

The job itself runs and succeeds (last_status: ok), but the report is never delivered and last_delivery_error is set. This is invisible from the creator's side.

Root cause

_bind_api_server_session hardwires platform="api_server" and async_delivery=False. There is no way to tell the cron mechanism "the live session channel can't deliver, but here's the real bridge platform that can."

Solution

Add a delivery-platform hint mechanism that decouples the two concerns:

  1. New context variable HERMES_SESSION_CRON_DELIVERY_PLATFORM — a hint used only by _origin_from_env when stamping the cron origin. It does NOT affect live-turn delivery or async_delivery.

  2. X-Hermes-Delivery-Platform header — api_server bridges set this to advertise their real sending platform (e.g. "telegram"). All request handlers extract it and pass it through to _bind_api_server_session.

  3. _origin_from_env override — when HERMES_SESSION_CRON_DELIVERY_PLATFORM is set, it overrides the origin platform so the cron job stamps its origin with the bridge's real sending platform.

Changes

  • gateway/session_context.py: Add _SESSION_CRON_DELIVERY_PLATFORM contextvar + cron_delivery_platform param to set_session_vars
  • gateway/platforms/api_server.py: Accept cron_delivery_platform in _bind_api_server_session and _run_agent; extract X-Hermes-Delivery-Platform header in all handlers
  • tools/cronjob_tools.py: Check HERMES_SESSION_CRON_DELIVERY_PLATFORM in _origin_from_env and override the origin platform

Usage

A bridge fronting the agent over /v1/chat/completions sets:

X-Hermes-Delivery-Platform: telegram

Cron jobs created from that session will now deliver back to the conversation they were created in.

Testing

  • All existing tests in tests/gateway/test_async_delivery_capability.py (15) and tests/gateway/test_session_context_inheritance.py (6) pass.

Closes #69304

webtecnica and others added 2 commits July 21, 2026 01:39
Previously _count_skills() only counted SKILL.md files inside the
profile's own skills/ directory, making the WebUI profile card show
a misleading low count (e.g. 0 for 'default', 30 for 'webtecnica')
even though the profile loaded 150+ skills from global + external
dirs.

Now it scans three sources:
1. Profile-specific skills/ dir (as before)
2. Global ~/.hermes/skills/ dir (via get_default_hermes_root)
3. External dirs from skills.external_dirs config

Deduplication by skill name (from YAML frontmatter) prevents double-
counting when the same skill exists in both global and profile dirs,
matching how scan_skill_commands() loads skills at runtime.

The cache is updated to key on all scanned directories and track
their combined mtime signatures.
…t for cron origin

When a cron job is created through the api_server platform, its origin is
stamped with platform="api_server" (from _origin_from_env →
HERMES_SESSION_PLATFORM).  At fire time, deliver=origin resolves to
the api_server adapter which has no send(), so the delivery silently
fails with API server uses HTTP request/response, not send() — even
though the job itself ran and succeeded.

Fix:
- Add _SESSION_CRON_DELIVERY_PLATFORM context variable to
  gateway/session_context.py.  This is a hint for the cron origin stamp
  only; it does NOT affect live-turn delivery or async_delivery.
- _bind_api_server_session now accepts an optional
  cron_delivery_platform parameter that is forwarded to
  set_session_vars.
- All api_server request handlers (_handle_chat_completions,
  _handle_responses, _handle_runs, _handle_session_chat,
  _handle_session_chat_stream) extract the X-Hermes-Delivery-Platform
  header and pass it through to _run_agent → _bind_api_server_session.
- _origin_from_env in cronjob_tools.py checks
  HERMES_SESSION_CRON_DELIVERY_PLATFORM: when set, it overrides the
  origin platform so the cron job stamps its origin with the bridge's real
  sending platform (e.g. "telegram") instead of "api_server".

This lets bridges (e.g. Chatto) that front the agent over
/v1/chat/completions set X-Hermes-Delivery-Platform: telegram (or
whatever their platform is) so cron jobs created from that bridge deliver
back to the conversation they were created in.

Closes NousResearch#69304
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/gateway Gateway runner, session dispatch, delivery comp/cron Cron scheduler and job management sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Jul 22, 2026

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for pursuing a real api_server cron-delivery gap. The current main still binds API turns as api_server (gateway/platforms/api_server.py:5742-5748), and deliver=origin later targets that stored platform/chat pair (cron/scheduler.py:1132-1138).

Problems

  • The new header changes only the platform. Current _run_agent() binds chat_id=session_id (gateway/platforms/api_server.py:5806-5810), and the scheduler sends that unchanged chat_id. An API transcript ID is not generally a Telegram/Discord destination, so X-Hermes-Delivery-Platform: telegram alone cannot establish a deliverable origin.
  • The /v1/runs path in this branch does not bind a chat ID, so the header cannot produce an origin there. Current main has subsequently added that binding at gateway/platforms/api_server.py:6290-6300; salvage must retain it.
  • The diff has no tests for the header/origin/delivery path and includes unrelated profile skill-count changes.

Suggested changes

  • Split the profile-count commit.
  • Carry or securely resolve a native platform destination (and thread metadata where applicable), validate it, and test every API agent-entry route with a send-capable adapter.

Automated hermes-sweeper review.

Comment thread tools/cronjob_tools.py
"Cron origin overriding platform %s -> %s (cron delivery hint)",
origin_platform, cron_delivery_platform,
)
origin_platform = cron_delivery_platform

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This changes only origin.platform; _run_agent still binds chat_id=session_id, and deliver=origin sends that chat ID unchanged. A bridge needs a validated native destination identity as well, otherwise a generated API transcript ID is used as a Telegram/Discord target.

@@ -4974,6 +5005,7 @@ def _run_sync():
approval_token = set_current_session_key(approval_session_key)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This /v1/runs binding passes the new hint but no chat_id or session_id, while _origin_from_env() requires both platform and chat_id. Current main has since added chat_id=session_id on this route; retain that behavior and add a route-level regression test when salvaging.

@teknium1 teknium1 added sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data 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 area/sessions Session lifecycle, resume, persistence, history labels Jul 30, 2026
@GottZ

GottZ commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

This was generated by AI during triage.

Summary

Three PRs address #69304 through distinct scopes: #69350 adds bridge-supplied origin routing, #69384 generalizes rejection and fallback for non-deliverable origins with creation-time visibility, and #69436 adds only an api_server-specific fallback.

Related pull requests

Duplicates

#69436 is substantially duplicated by the scheduler portion of #69384. #69350 is not a duplicate because it implements the competing bridge-supplied routing policy documented in the contributor discussion.

Suggested consolidation

Keep #69350 open with a salvage path, consistent with the contributor keep_open review: carry or securely resolve both the native platform and destination identity, preserve current-main /v1/runs chat/session binding, add route-level delivery tests, and split out the unrelated profiles change. Leave #69384 closed as the recorded best-fix reference for the fallback policy, and leave #69436 closed as its narrower duplicate.

Complex graph

flowchart LR
    classDef open fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
    classDef merged fill:#dcfce7,stroke:#15803d,color:#14532d
    classDef closed fill:#e5e7eb,stroke:#6b7280,color:#1f2937
    classDef unverified fill:#f3f4f6,stroke:#9ca3af,color:#374151
    classDef best stroke-width:3px,stroke:#b45309
    classDef target stroke-width:3px,stroke:#4338ca
    I69304(["issue #69304 (open)"])
    P69350["PR #69350 (open)"]
    P69350 -.->|partial| I69304
    class I69304 open
    class P69350 open
    class P69350 target
    click I69304 "https://github.com/NousResearch/hermes-agent/issues/69304"
    click P69350 "https://github.com/NousResearch/hermes-agent/pull/69350"
Loading

Graph: solid arrow = fixes / best fix, dashed arrow = partial or unverified (see edge label); boxed group = PRs duplicating each other; amber border = best fix; indigo border = target; gray node = closed (state tag in the node label).

Cross-PR triage: Reviewed 3 pull requests and 1 issue in this complex. Each diff was read against this issue; Assessment working set: 31 kB of PR diffs, 10 kB of issue/PR text, 5 kB of discussion (10 comments), 4 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch.

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

Labels

area/sessions Session lifecycle, resume, persistence, history comp/cron Cron scheduler and job management comp/gateway Gateway runner, session dispatch, delivery 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 sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cron deliver=origin silently fails for jobs created in an api_server session (origin platform can't send)

4 participants