Skip to content

fix: scheduler was not attaching media files#4877

Closed
robert-hoffmann wants to merge 2 commits into
NousResearch:mainfrom
robert-hoffmann:main
Closed

fix: scheduler was not attaching media files#4877
robert-hoffmann wants to merge 2 commits into
NousResearch:mainfrom
robert-hoffmann:main

Conversation

@robert-hoffmann

Copy link
Copy Markdown

fix(scheduler): forward media_files to _send_to_platform()

What does this PR do?

Fixes cron delivery so media attachments are forwarded correctly when scheduler jobs send results through _send_to_platform().

The underlying send path in tools/send_message_tool.py already supported MEDIA: extraction and native file delivery, but the cron scheduler delivery path was still only passing message text. As a result, cron jobs could deliver cleaned text while dropping the extracted attachment payload.

This change updates cron/scheduler.py to:

  • extract media from the rendered delivery content,
  • pass cleaned_delivery_content to _send_to_platform(), and
  • forward media_files=media_files so attachments are actually delivered.

While touching this path, the scheduler also received focused cleanup around documentation, environment handling, and delivery-flow readability, without changing the intended behavior outside this fix.

Related Issue

Fixes #

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • Updated cron/scheduler.py so cron delivery forwards extracted media_files into _send_to_platform()
  • Switched scheduler delivery to send cleaned_delivery_content instead of the raw wrapped body after media extraction
  • Added/updated regression coverage in tests/cron/test_scheduler.py to verify cron delivery passes attachments separately from message text
  • Cleaned up scheduler delivery internals and improved local documentation/docstrings in touched regions

How to Test

  1. Run the focused scheduler test suite:

    python -m pytest tests/cron/test_scheduler.py -q -n 0

…mproved environment handling

fix(schedfuler): media files not being attached via _send_to_platform()
Copilot AI review requested due to automatic review settings April 3, 2026 21:37

Copilot AI 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.

Pull request overview

This PR fixes cron scheduler deliveries so extracted MEDIA: attachments are forwarded to the shared _send_to_platform() send path, aligning scheduler behavior with the existing send_message_tool media handling.

Changes:

  • Update cron/scheduler.py to extract MEDIA: tags via BasePlatformAdapter.extract_media() and pass media_files + cleaned text into _send_to_platform().
  • Refactor scheduler internals for clearer delivery flow and improved env injection/cleanup helpers.
  • Add a regression test in tests/cron/test_scheduler.py verifying that cron delivery passes attachments separately from message text.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
cron/scheduler.py Extracts media before delivery and forwards media_files to _send_to_platform(); refactors delivery/tick flow.
tests/cron/test_scheduler.py Adds/updates tests to assert cleaned content + media_files forwarding in cron delivery.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread cron/scheduler.py
Comment thread cron/scheduler.py
Comment on lines +303 to +307
media_files, cleaned_delivery_content = BasePlatformAdapter.extract_media(delivery_content)

# Run the async send in a fresh event loop (safe from any thread)
coro = _send_to_platform(platform, pconfig, chat_id, delivery_content, thread_id=thread_id)
coro = _send_to_platform(
platform,

Copilot AI Apr 3, 2026

Copy link

Choose a reason for hiding this comment

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

BasePlatformAdapter.extract_media() can produce an empty cleaned_delivery_content when the message is only MEDIA: tags. Passing media_files with an empty message into _send_to_platform() will return an error for non-Telegram platforms (it only supports native media on Telegram), causing cron delivery to silently fail where it previously would have delivered the tag text. Consider extracting/passing media_files only for Telegram, or falling back to sending the original delivery_content (or a textual placeholder) when platform != Platform.TELEGRAM and cleaned_delivery_content.strip() is empty.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This is an existing problem, and should probably be patched in another PR, and most likely be patched in _send_to_platform() directly

My agents response after questioning him:

This is a real issue, but it is narrower than it first sounds.

The cron scheduler extracts `MEDIA:` tags before calling `_send_to_platform()`. On Telegram, that is fine because `_send_to_platform()` supports native media delivery there. On non-Telegram platforms, `_send_to_platform()` explicitly rejects the case where it receives `media_files` with an empty text body.

That means the failure case is:

- the cron response is only `MEDIA:` tags, or `[[audio_as_voice]]` plus `MEDIA:`
- `cron.wrap_response` is disabled
- the delivery target is not Telegram

In that path, cron strips the tags, ends up with an empty message, passes the extracted media into `_send_to_platform()`, and the non-Telegram branch returns an error instead of falling back to text. The scheduler logs the error, but there is no user-facing fallback, so delivery can appear to silently fail.

A useful nuance: with the default config, `cron.wrap_response` is enabled, so cron usually adds header/footer text around the response before media extraction. In that default path, the cleaned message is usually not empty, so delivery does not completely fail. However, the media is still omitted on non-Telegram platforms, and the warning about that omission is not surfaced to the end user. So the default behavior is usually degraded delivery, not total delivery failure.

So the right conclusion is:

- yes, this is a real bug
- no, it is not a broad default-path outage
- it is primarily an edge-case failure for media-only cron output on non-Telegram targets
- even outside that edge case, non-Telegram cron delivery can silently lose media content

The cleanest fixes would be either:

1. In cron delivery, only extract and pass `media_files` for Telegram, and otherwise send the original text or a fallback placeholder when media stripping leaves the message empty.
2. In `_send_to_platform()`, make non-Telegram media-only sends degrade gracefully to text instead of returning an error.

I would treat this as worth fixing because the current behavior is inconsistent and can fail in a way that is hard for users to understand.

Comment thread cron/scheduler.py
Comment on lines +719 to 721
except OSError:
logger.debug("Tick skipped — another instance holds the lock")
if lock_fd is not None:
lock_fd.close()
return 0

Copilot AI Apr 3, 2026

Copy link

Choose a reason for hiding this comment

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

The outer except OSError: around the whole tick body will also catch unrelated filesystem errors from get_due_jobs() / job processing and misreport them as “another instance holds the lock”, returning 0 and hiding the real failure. Please narrow this handler to only lock-acquisition/locking operations (or log the exception details when it’s not a lock contention case).

Copilot uses AI. Check for mistakes.
@robert-hoffmann

Copy link
Copy Markdown
Author

The media attachment problem is _send_to_platform was missing media_files=media_files as the last argument

I also did some cleanup

Everything is GREEN with pylance in standards mode and ruff using the following settings

select = [
    "E",    # pycodestyle errors
    "W",    # pycodestyle warnings
    "F",    # Pyflakes
    "I",    # isort (import order)
    "N",    # pep8-naming
    "UP",   # pyupgrade — modernise syntax
    "B",    # flake8-bugbear
    "C4",   # flake8-comprehensions
    "SIM",  # flake8-simplify
    "TID",  # flake8-tidy-imports
    "RUF",  # Ruff-specific rules
    # ── Hardening ─────────────────────────
    "S",    # flake8-bandit — security
    "A",    # flake8-builtins — no shadowing builtins
    "DTZ",  # flake8-datetimez — timezone-aware datetimes
    "PGH",  # pygrep-hooks — no blanket type: ignore / noqa
    "FA",   # flake8-future-annotations
    "PERF", # perflint — performance anti-patterns
    "PLE",  # pylint errors
    "PLW",  # pylint warnings
    # ── Quality ───────────────────────────
    "PIE",  # flake8-pie — misc lint
    "RSE",  # flake8-raise
    "ISC",  # implicit-string-concat
    "FLY",  # flynt — f-string conversion
    "FURB", # refurb — modern Python
]

@robert-hoffmann

Copy link
Copy Markdown
Author

PS. ya'll will need to update the node runtimes in the test.yml workflows : GH is failing with

Node.js 20 actions are deprecated. The following actions are running on Node.js 20 and may not work as expected: actions/checkout@v4, astral-sh/setup-uv@v5. Actions will be forced to run with Node.js 24 by default starting June 2nd, 2026. Node.js 20 will be removed from the runner on September 16th, 2026.

@robert-hoffmann

robert-hoffmann commented Apr 3, 2026

Copy link
Copy Markdown
Author

I'd also move to at least python 3.12

It brings some pretty big performance improvements, especially around async stuff, which can be interresting for agents and jobs

teknium1 added a commit that referenced this pull request Apr 6, 2026
The cron scheduler delivery path passed raw text including MEDIA: tags
to _send_to_platform(), so media attachments were delivered as literal
text instead of actual files. The send function already supports
media_files= but the cron path never used it.

Now calls BasePlatformAdapter.extract_media() to split media paths
from text before sending, matching the gateway's normal message flow.

Salvaged from PR #4877 by robert-hoffmann.
teknium1 added a commit that referenced this pull request Apr 6, 2026
The cron scheduler delivery path passed raw text including MEDIA: tags
to _send_to_platform(), so media attachments were delivered as literal
text instead of actual files. The send function already supports
media_files= but the cron path never used it.

Now calls BasePlatformAdapter.extract_media() to split media paths
from text before sending, matching the gateway's normal message flow.

Salvaged from PR #4877 by robert-hoffmann.
@teknium1

teknium1 commented Apr 6, 2026

Copy link
Copy Markdown
Contributor

Merged via PR #5598. Your media extraction fix was cherry-picked onto current main — the core fix (extract_media + media_files forwarding) is preserved. The unrelated cosmetic/refactoring changes were skipped to keep the diff focused. Thanks for catching this bug, @robert-hoffmann!

@teknium1 teknium1 closed this Apr 6, 2026
@robert-hoffmann

robert-hoffmann commented Apr 6, 2026

Copy link
Copy Markdown
Author

Something didn't go right @teknium1

I updated to the latest version which includes the patch, but something changed in the code and the attachment still doesnt go through, seems there were other code changes in the mean-time..

Here is Herme's diagnostic:

Yes — I checked, and I did not make any further changes.

Why it failed:
- The cron job itself worked.
- It generated the text summary.
- It generated the .ogg voice file.
- The final cron output included a proper line.

So the failure was specifically in the delivery path.

The key issue:
- In cron/scheduler.py, the scheduler does extract the MEDIA tags first.
- But when the gateway is already running, it prefers the “live adapter” path.
- In that live path, it calls:
  runtime_adapter.send(chat_id, delivery_content, metadata=...)
- That send() call is a text send.
- It does not separately forward the extracted media_files in that branch.

So in practice:
- text message gets delivered
- voice file is not attached in that same cron delivery
- manual resend works because that path sends the file directly

Why I’m confident that’s the cause:
1) Your Nate cron output file shows the final response contained:
   

2) The actual audio file exists on disk:
   /Users/.../.hermes/cron/natebjones_7HP1jFJ9W1c_summary.ogg

3) The scheduler source shows two different delivery branches:
- live adapter branch sends runtime_adapter.send(...)
- standalone branch sends _send_to_platform(..., media_files=media_files)

Only the standalone branch clearly passes media_files through.

So the most likely explanation is:
the cron run went through the live adapter branch, which delivered the text but dropped the attachment.

Short version:
The cron job didn’t fail to make the voice note.
It failed to attach it during auto-delivery, because the live cron delivery path appears to send only the text portion.

If you want, I can also explain the difference between the “live adapter” path and the “standalone” delivery path in plain English.

Tommyeds pushed a commit to Tommyeds/hermes-agent that referenced this pull request Apr 12, 2026
…rch#5598)

The cron scheduler delivery path passed raw text including MEDIA: tags
to _send_to_platform(), so media attachments were delivered as literal
text instead of actual files. The send function already supports
media_files= but the cron path never used it.

Now calls BasePlatformAdapter.extract_media() to split media paths
from text before sending, matching the gateway's normal message flow.

Salvaged from PR NousResearch#4877 by robert-hoffmann.
angelburgosrosado pushed a commit to angelburgosrosado/hermes-agent that referenced this pull request Apr 27, 2026
…rch#5598)

The cron scheduler delivery path passed raw text including MEDIA: tags
to _send_to_platform(), so media attachments were delivered as literal
text instead of actual files. The send function already supports
media_files= but the cron path never used it.

Now calls BasePlatformAdapter.extract_media() to split media paths
from text before sending, matching the gateway's normal message flow.

Salvaged from PR NousResearch#4877 by robert-hoffmann.
angelburgosrosado pushed a commit to angelburgosrosado/hermes-agent that referenced this pull request Apr 28, 2026
The cron scheduler delivery path passed raw text including MEDIA: tags
to _send_to_platform(), so media attachments were delivered as literal
text instead of actual files. The send function already supports
media_files= but the cron path never used it.

Now calls BasePlatformAdapter.extract_media() to split media paths
from text before sending, matching the gateway's normal message flow.

Salvaged from PR NousResearch#4877 by robert-hoffmann.
02356abc pushed a commit to 02356abc/hermes-agent that referenced this pull request May 14, 2026
…rch#5598)

The cron scheduler delivery path passed raw text including MEDIA: tags
to _send_to_platform(), so media attachments were delivered as literal
text instead of actual files. The send function already supports
media_files= but the cron path never used it.

Now calls BasePlatformAdapter.extract_media() to split media paths
from text before sending, matching the gateway's normal message flow.

Salvaged from PR NousResearch#4877 by robert-hoffmann.
olympus-terminal pushed a commit to olympus-terminal/hermes-agent that referenced this pull request May 16, 2026
…rch#5598)

The cron scheduler delivery path passed raw text including MEDIA: tags
to _send_to_platform(), so media attachments were delivered as literal
text instead of actual files. The send function already supports
media_files= but the cron path never used it.

Now calls BasePlatformAdapter.extract_media() to split media paths
from text before sending, matching the gateway's normal message flow.

Salvaged from PR NousResearch#4877 by robert-hoffmann.
gweeteve pushed a commit to gweeteve/hermes-agent that referenced this pull request Jun 2, 2026
…rch#5598)

The cron scheduler delivery path passed raw text including MEDIA: tags
to _send_to_platform(), so media attachments were delivered as literal
text instead of actual files. The send function already supports
media_files= but the cron path never used it.

Now calls BasePlatformAdapter.extract_media() to split media paths
from text before sending, matching the gateway's normal message flow.

Salvaged from PR NousResearch#4877 by robert-hoffmann.
Egavasyug pushed a commit to Egavasyug/hermes-agent that referenced this pull request Jun 10, 2026
…rch#5598)

The cron scheduler delivery path passed raw text including MEDIA: tags
to _send_to_platform(), so media attachments were delivered as literal
text instead of actual files. The send function already supports
media_files= but the cron path never used it.

Now calls BasePlatformAdapter.extract_media() to split media paths
from text before sending, matching the gateway's normal message flow.

Salvaged from PR NousResearch#4877 by robert-hoffmann.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants