Skip to content

Fix #38922: Cron delivery confirmation timeout causes duplicate message - #41007

Closed
iamlukethedev wants to merge 1 commit into
NousResearch:mainfrom
iamlukethedev:fix/38922-cron-delivery-duplicate
Closed

Fix #38922: Cron delivery confirmation timeout causes duplicate message#41007
iamlukethedev wants to merge 1 commit into
NousResearch:mainfrom
iamlukethedev:fix/38922-cron-delivery-duplicate

Conversation

@iamlukethedev

Copy link
Copy Markdown
Contributor

Fixes #38922: When the live adapter sends a cron message but its confirmation
times out (>60s), the code was treating this as send-failure and re-sending via
the standalone path, resulting in a duplicate message.

PROBLEM:

  • Cron job sends message via live adapter (dispatched to gateway event loop)
  • Message in flight on the wire ✓
  • Confirmation response doesn't return within 60s timeout
  • TimeoutError raised → exception caught → fallback to standalone send
  • RESULT: Same message sent twice (duplicate)

ROOT CAUSE:

  • future.result(timeout=60) raises TimeoutError
  • future.cancel() called (non-functional — message already on wire)
  • Exception re-raised → caught by outer except handler
  • Fallback to standalone path triggered (line 886: if not delivered:)

SOLUTION:
Catch TimeoutError specially (not as generic Exception):

  1. Recognize that TimeoutError means confirmation was slow, NOT send failure
  2. Message was already dispatched to wire before timeout occurred
  3. Set delivered = True to skip the standalone fallback
  4. Set send_result = None to avoid unbound variable issues
  5. Log warning about the timeout (for diagnostics)

WHY THIS IS SAFE:

  • When TimeoutError occurs, the coro was already scheduled and dispatched
  • The send request is definitely in flight on the wire
  • Waiting longer won't help (60s is already generous for confirmation)
  • Re-sending creates a guaranteed duplicate, not a recovery

IMPACT:

  • ✅ Cron deliveries no longer duplicate on slow confirmation (>60s)
  • ✅ Single send with warning logged
  • ✅ No silent drops or missed messages
  • ✅ Works for all platforms (Telegram, Discord, Slack, etc.)

Test coverage:

  • Updated TestDeliverResultTimeoutCancelsFuture to verify NO duplicate send
  • Verified standalone path is NOT called when timeout occurs
  • Verified future.cancel() is NOT called (it's non-functional anyway)
  • All 406 cron tests passing with zero regressions

Fixes #38922

…licate message

Fixes NousResearch#38922: When the live adapter sends a cron message but its confirmation
times out (>60s), the code was treating this as send-failure and re-sending via
the standalone path, resulting in a duplicate message.

PROBLEM:
- Cron job sends message via live adapter (dispatched to gateway event loop)
- Message in flight on the wire ✓
- Confirmation response doesn't return within 60s timeout
- TimeoutError raised → exception caught → fallback to standalone send
- RESULT: Same message sent twice (duplicate)

ROOT CAUSE:
- future.result(timeout=60) raises TimeoutError
- future.cancel() called (non-functional — message already on wire)
- Exception re-raised → caught by outer except handler
- Fallback to standalone path triggered (line 886: if not delivered:)

SOLUTION:
Catch TimeoutError specially (not as generic Exception):
1. Recognize that TimeoutError means confirmation was slow, NOT send failure
2. Message was already dispatched to wire before timeout occurred
3. Set delivered = True to skip the standalone fallback
4. Set send_result = None to avoid unbound variable issues
5. Log warning about the timeout (for diagnostics)

WHY THIS IS SAFE:
- When TimeoutError occurs, the coro was already scheduled and dispatched
- The send request is definitely in flight on the wire
- Waiting longer won't help (60s is already generous for confirmation)
- Re-sending creates a guaranteed duplicate, not a recovery

IMPACT:
- ✅ Cron deliveries no longer duplicate on slow confirmation (>60s)
- ✅ Single send with warning logged
- ✅ No silent drops or missed messages
- ✅ Works for all platforms (Telegram, Discord, Slack, etc.)

Test coverage:
- Updated TestDeliverResultTimeoutCancelsFuture to verify NO duplicate send
- Verified standalone path is NOT called when timeout occurs
- Verified future.cancel() is NOT called (it's non-functional anyway)
- All 406 cron tests passing with zero regressions

Fixes NousResearch#38922
@alt-glitch alt-glitch added type/bug Something isn't working comp/cron Cron scheduler and job management P2 Medium — degraded but workaround exists labels Jun 7, 2026
kshitijk4poor pushed a commit that referenced this pull request Jun 21, 2026
…47056, #43014)

Consolidates three cron-delivery defects in cron/scheduler.py::_deliver_result
that all stem from how the live-adapter send result is interpreted.

#38922 — duplicate message on confirmation timeout.
  future.result(timeout=60) raising TimeoutError bubbled to the outer
  except handler, which left delivered=False, so `if not delivered:` re-sent
  the identical message via the standalone path. future.cancel() cannot
  un-send a request already in flight on the wire, so a slow confirmation
  deterministically produced a duplicate. The send was already dispatched onto
  the gateway loop, so a bare timeout is now treated as delivered
  (assume-delivered is safer than guaranteed-duplicate) and the standalone
  fallback is skipped. The live-adapter media attempt is also skipped on
  timeout since the contended loop would re-block each 30s media budget.

#47056 — silent drop when the gateway has an active session.
  The old check `if send_result is None or not getattr(send_result,
  "success", True)` let a result object missing a `success` attribute default
  to True = counted as a successful delivery, so the scheduler logged
  "delivered via live adapter" while the gateway never processed the message.
  Delivery is now confirmed via _confirm_adapter_delivery(): only an explicit,
  truthy `success` attribute counts; None or a `success`-less object falls
  through to the standalone path so the message actually arrives.

  A genuine send Exception (not a slow confirmation) still falls through to
  the standalone path, and is caught by run_job's outer handler — it is
  recorded as the job's last_error and never crashes the cron ticker.

#43014 — deliver=origin fails to resolve in CLI sessions.
  A CLI-created job has no {platform, chat_id} origin, so deliver=origin (and
  auto-detect / deliver=None) was unresolvable and emitted "no delivery target
  resolved" on every run. An unresolvable origin with no configured home
  channel is now treated as local (output stays in last_output), matching the
  documented auto-deliver contract; a concrete unresolvable platform target
  still reports a real error.

Salvaged from #41007 (timeout discriminator), folding in #47127's
_confirm_adapter_delivery hardening and #38937 / #43063's origin→local
fallback. Tests rewritten as behavior contracts (timeout => no duplicate;
None / success-less result => standalone fallback; confirmed success => no
fallback; CLI origin => local, explicit platform => still errors).

Co-authored-by: Evi Nova <66773372+Tranquil-Flow@users.noreply.github.com>
Co-authored-by: kyssta-exe <kyssta-exe@users.noreply.github.com>
kpadilha pushed a commit to kpadilha/hermes-agent that referenced this pull request Jun 24, 2026
…arch#38922, NousResearch#47056, NousResearch#43014)

Consolidates three cron-delivery defects in cron/scheduler.py::_deliver_result
that all stem from how the live-adapter send result is interpreted.

NousResearch#38922 — duplicate message on confirmation timeout.
  future.result(timeout=60) raising TimeoutError bubbled to the outer
  except handler, which left delivered=False, so `if not delivered:` re-sent
  the identical message via the standalone path. future.cancel() cannot
  un-send a request already in flight on the wire, so a slow confirmation
  deterministically produced a duplicate. The send was already dispatched onto
  the gateway loop, so a bare timeout is now treated as delivered
  (assume-delivered is safer than guaranteed-duplicate) and the standalone
  fallback is skipped. The live-adapter media attempt is also skipped on
  timeout since the contended loop would re-block each 30s media budget.

NousResearch#47056 — silent drop when the gateway has an active session.
  The old check `if send_result is None or not getattr(send_result,
  "success", True)` let a result object missing a `success` attribute default
  to True = counted as a successful delivery, so the scheduler logged
  "delivered via live adapter" while the gateway never processed the message.
  Delivery is now confirmed via _confirm_adapter_delivery(): only an explicit,
  truthy `success` attribute counts; None or a `success`-less object falls
  through to the standalone path so the message actually arrives.

  A genuine send Exception (not a slow confirmation) still falls through to
  the standalone path, and is caught by run_job's outer handler — it is
  recorded as the job's last_error and never crashes the cron ticker.

NousResearch#43014 — deliver=origin fails to resolve in CLI sessions.
  A CLI-created job has no {platform, chat_id} origin, so deliver=origin (and
  auto-detect / deliver=None) was unresolvable and emitted "no delivery target
  resolved" on every run. An unresolvable origin with no configured home
  channel is now treated as local (output stays in last_output), matching the
  documented auto-deliver contract; a concrete unresolvable platform target
  still reports a real error.

Salvaged from NousResearch#41007 (timeout discriminator), folding in NousResearch#47127's
_confirm_adapter_delivery hardening and NousResearch#38937 / NousResearch#43063's origin→local
fallback. Tests rewritten as behavior contracts (timeout => no duplicate;
None / success-less result => standalone fallback; confirmed success => no
fallback; CLI origin => local, explicit platform => still errors).

Co-authored-by: Evi Nova <66773372+Tranquil-Flow@users.noreply.github.com>
Co-authored-by: kyssta-exe <kyssta-exe@users.noreply.github.com>
pai-scaffolde pushed a commit to pai-scaffolde/hermes-agent that referenced this pull request Jun 28, 2026
…arch#38922, NousResearch#47056, NousResearch#43014)

Consolidates three cron-delivery defects in cron/scheduler.py::_deliver_result
that all stem from how the live-adapter send result is interpreted.

NousResearch#38922 — duplicate message on confirmation timeout.
  future.result(timeout=60) raising TimeoutError bubbled to the outer
  except handler, which left delivered=False, so `if not delivered:` re-sent
  the identical message via the standalone path. future.cancel() cannot
  un-send a request already in flight on the wire, so a slow confirmation
  deterministically produced a duplicate. The send was already dispatched onto
  the gateway loop, so a bare timeout is now treated as delivered
  (assume-delivered is safer than guaranteed-duplicate) and the standalone
  fallback is skipped. The live-adapter media attempt is also skipped on
  timeout since the contended loop would re-block each 30s media budget.

NousResearch#47056 — silent drop when the gateway has an active session.
  The old check `if send_result is None or not getattr(send_result,
  "success", True)` let a result object missing a `success` attribute default
  to True = counted as a successful delivery, so the scheduler logged
  "delivered via live adapter" while the gateway never processed the message.
  Delivery is now confirmed via _confirm_adapter_delivery(): only an explicit,
  truthy `success` attribute counts; None or a `success`-less object falls
  through to the standalone path so the message actually arrives.

  A genuine send Exception (not a slow confirmation) still falls through to
  the standalone path, and is caught by run_job's outer handler — it is
  recorded as the job's last_error and never crashes the cron ticker.

NousResearch#43014 — deliver=origin fails to resolve in CLI sessions.
  A CLI-created job has no {platform, chat_id} origin, so deliver=origin (and
  auto-detect / deliver=None) was unresolvable and emitted "no delivery target
  resolved" on every run. An unresolvable origin with no configured home
  channel is now treated as local (output stays in last_output), matching the
  documented auto-deliver contract; a concrete unresolvable platform target
  still reports a real error.

Salvaged from NousResearch#41007 (timeout discriminator), folding in NousResearch#47127's
_confirm_adapter_delivery hardening and NousResearch#38937 / NousResearch#43063's origin→local
fallback. Tests rewritten as behavior contracts (timeout => no duplicate;
None / success-less result => standalone fallback; confirmed success => no
fallback; CLI origin => local, explicit platform => still errors).

Co-authored-by: Evi Nova <66773372+Tranquil-Flow@users.noreply.github.com>
Co-authored-by: kyssta-exe <kyssta-exe@users.noreply.github.com>
indigokarasu pushed a commit to indigokarasu/hermes-agent that referenced this pull request Jul 1, 2026
…arch#38922, NousResearch#47056, NousResearch#43014)

Consolidates three cron-delivery defects in cron/scheduler.py::_deliver_result
that all stem from how the live-adapter send result is interpreted.

NousResearch#38922 — duplicate message on confirmation timeout.
  future.result(timeout=60) raising TimeoutError bubbled to the outer
  except handler, which left delivered=False, so `if not delivered:` re-sent
  the identical message via the standalone path. future.cancel() cannot
  un-send a request already in flight on the wire, so a slow confirmation
  deterministically produced a duplicate. The send was already dispatched onto
  the gateway loop, so a bare timeout is now treated as delivered
  (assume-delivered is safer than guaranteed-duplicate) and the standalone
  fallback is skipped. The live-adapter media attempt is also skipped on
  timeout since the contended loop would re-block each 30s media budget.

NousResearch#47056 — silent drop when the gateway has an active session.
  The old check `if send_result is None or not getattr(send_result,
  "success", True)` let a result object missing a `success` attribute default
  to True = counted as a successful delivery, so the scheduler logged
  "delivered via live adapter" while the gateway never processed the message.
  Delivery is now confirmed via _confirm_adapter_delivery(): only an explicit,
  truthy `success` attribute counts; None or a `success`-less object falls
  through to the standalone path so the message actually arrives.

  A genuine send Exception (not a slow confirmation) still falls through to
  the standalone path, and is caught by run_job's outer handler — it is
  recorded as the job's last_error and never crashes the cron ticker.

NousResearch#43014 — deliver=origin fails to resolve in CLI sessions.
  A CLI-created job has no {platform, chat_id} origin, so deliver=origin (and
  auto-detect / deliver=None) was unresolvable and emitted "no delivery target
  resolved" on every run. An unresolvable origin with no configured home
  channel is now treated as local (output stays in last_output), matching the
  documented auto-deliver contract; a concrete unresolvable platform target
  still reports a real error.

Salvaged from NousResearch#41007 (timeout discriminator), folding in NousResearch#47127's
_confirm_adapter_delivery hardening and NousResearch#38937 / NousResearch#43063's origin→local
fallback. Tests rewritten as behavior contracts (timeout => no duplicate;
None / success-less result => standalone fallback; confirmed success => no
fallback; CLI origin => local, explicit platform => still errors).

Co-authored-by: Evi Nova <66773372+Tranquil-Flow@users.noreply.github.com>
Co-authored-by: kyssta-exe <kyssta-exe@users.noreply.github.com>
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
…arch#38922, NousResearch#47056, NousResearch#43014)

Consolidates three cron-delivery defects in cron/scheduler.py::_deliver_result
that all stem from how the live-adapter send result is interpreted.

NousResearch#38922 — duplicate message on confirmation timeout.
  future.result(timeout=60) raising TimeoutError bubbled to the outer
  except handler, which left delivered=False, so `if not delivered:` re-sent
  the identical message via the standalone path. future.cancel() cannot
  un-send a request already in flight on the wire, so a slow confirmation
  deterministically produced a duplicate. The send was already dispatched onto
  the gateway loop, so a bare timeout is now treated as delivered
  (assume-delivered is safer than guaranteed-duplicate) and the standalone
  fallback is skipped. The live-adapter media attempt is also skipped on
  timeout since the contended loop would re-block each 30s media budget.

NousResearch#47056 — silent drop when the gateway has an active session.
  The old check `if send_result is None or not getattr(send_result,
  "success", True)` let a result object missing a `success` attribute default
  to True = counted as a successful delivery, so the scheduler logged
  "delivered via live adapter" while the gateway never processed the message.
  Delivery is now confirmed via _confirm_adapter_delivery(): only an explicit,
  truthy `success` attribute counts; None or a `success`-less object falls
  through to the standalone path so the message actually arrives.

  A genuine send Exception (not a slow confirmation) still falls through to
  the standalone path, and is caught by run_job's outer handler — it is
  recorded as the job's last_error and never crashes the cron ticker.

NousResearch#43014 — deliver=origin fails to resolve in CLI sessions.
  A CLI-created job has no {platform, chat_id} origin, so deliver=origin (and
  auto-detect / deliver=None) was unresolvable and emitted "no delivery target
  resolved" on every run. An unresolvable origin with no configured home
  channel is now treated as local (output stays in last_output), matching the
  documented auto-deliver contract; a concrete unresolvable platform target
  still reports a real error.

Salvaged from NousResearch#41007 (timeout discriminator), folding in NousResearch#47127's
_confirm_adapter_delivery hardening and NousResearch#38937 / NousResearch#43063's origin→local
fallback. Tests rewritten as behavior contracts (timeout => no duplicate;
None / success-less result => standalone fallback; confirmed success => no
fallback; CLI origin => local, explicit platform => still errors).

Co-authored-by: Evi Nova <66773372+Tranquil-Flow@users.noreply.github.com>
Co-authored-by: kyssta-exe <kyssta-exe@users.noreply.github.com>
habarmc1223-sudo pushed a commit to habarmc1223-sudo/hermes-agent-fluxmem that referenced this pull request Jul 8, 2026
…arch#38922, NousResearch#47056, NousResearch#43014)

Consolidates three cron-delivery defects in cron/scheduler.py::_deliver_result
that all stem from how the live-adapter send result is interpreted.

NousResearch#38922 — duplicate message on confirmation timeout.
  future.result(timeout=60) raising TimeoutError bubbled to the outer
  except handler, which left delivered=False, so `if not delivered:` re-sent
  the identical message via the standalone path. future.cancel() cannot
  un-send a request already in flight on the wire, so a slow confirmation
  deterministically produced a duplicate. The send was already dispatched onto
  the gateway loop, so a bare timeout is now treated as delivered
  (assume-delivered is safer than guaranteed-duplicate) and the standalone
  fallback is skipped. The live-adapter media attempt is also skipped on
  timeout since the contended loop would re-block each 30s media budget.

NousResearch#47056 — silent drop when the gateway has an active session.
  The old check `if send_result is None or not getattr(send_result,
  "success", True)` let a result object missing a `success` attribute default
  to True = counted as a successful delivery, so the scheduler logged
  "delivered via live adapter" while the gateway never processed the message.
  Delivery is now confirmed via _confirm_adapter_delivery(): only an explicit,
  truthy `success` attribute counts; None or a `success`-less object falls
  through to the standalone path so the message actually arrives.

  A genuine send Exception (not a slow confirmation) still falls through to
  the standalone path, and is caught by run_job's outer handler — it is
  recorded as the job's last_error and never crashes the cron ticker.

NousResearch#43014 — deliver=origin fails to resolve in CLI sessions.
  A CLI-created job has no {platform, chat_id} origin, so deliver=origin (and
  auto-detect / deliver=None) was unresolvable and emitted "no delivery target
  resolved" on every run. An unresolvable origin with no configured home
  channel is now treated as local (output stays in last_output), matching the
  documented auto-deliver contract; a concrete unresolvable platform target
  still reports a real error.

Salvaged from NousResearch#41007 (timeout discriminator), folding in NousResearch#47127's
_confirm_adapter_delivery hardening and NousResearch#38937 / NousResearch#43063's origin→local
fallback. Tests rewritten as behavior contracts (timeout => no duplicate;
None / success-less result => standalone fallback; confirmed success => no
fallback; CLI origin => local, explicit platform => still errors).

Co-authored-by: Evi Nova <66773372+Tranquil-Flow@users.noreply.github.com>
Co-authored-by: kyssta-exe <kyssta-exe@users.noreply.github.com>
santhreal pushed a commit to santhreal/hermes-agent that referenced this pull request Jul 13, 2026
…arch#38922, NousResearch#47056, NousResearch#43014)

Consolidates three cron-delivery defects in cron/scheduler.py::_deliver_result
that all stem from how the live-adapter send result is interpreted.

NousResearch#38922 — duplicate message on confirmation timeout.
  future.result(timeout=60) raising TimeoutError bubbled to the outer
  except handler, which left delivered=False, so `if not delivered:` re-sent
  the identical message via the standalone path. future.cancel() cannot
  un-send a request already in flight on the wire, so a slow confirmation
  deterministically produced a duplicate. The send was already dispatched onto
  the gateway loop, so a bare timeout is now treated as delivered
  (assume-delivered is safer than guaranteed-duplicate) and the standalone
  fallback is skipped. The live-adapter media attempt is also skipped on
  timeout since the contended loop would re-block each 30s media budget.

NousResearch#47056 — silent drop when the gateway has an active session.
  The old check `if send_result is None or not getattr(send_result,
  "success", True)` let a result object missing a `success` attribute default
  to True = counted as a successful delivery, so the scheduler logged
  "delivered via live adapter" while the gateway never processed the message.
  Delivery is now confirmed via _confirm_adapter_delivery(): only an explicit,
  truthy `success` attribute counts; None or a `success`-less object falls
  through to the standalone path so the message actually arrives.

  A genuine send Exception (not a slow confirmation) still falls through to
  the standalone path, and is caught by run_job's outer handler — it is
  recorded as the job's last_error and never crashes the cron ticker.

NousResearch#43014 — deliver=origin fails to resolve in CLI sessions.
  A CLI-created job has no {platform, chat_id} origin, so deliver=origin (and
  auto-detect / deliver=None) was unresolvable and emitted "no delivery target
  resolved" on every run. An unresolvable origin with no configured home
  channel is now treated as local (output stays in last_output), matching the
  documented auto-deliver contract; a concrete unresolvable platform target
  still reports a real error.

Salvaged from NousResearch#41007 (timeout discriminator), folding in NousResearch#47127's
_confirm_adapter_delivery hardening and NousResearch#38937 / NousResearch#43063's origin→local
fallback. Tests rewritten as behavior contracts (timeout => no duplicate;
None / success-less result => standalone fallback; confirmed success => no
fallback; CLI origin => local, explicit platform => still errors).

Co-authored-by: Evi Nova <66773372+Tranquil-Flow@users.noreply.github.com>
Co-authored-by: kyssta-exe <kyssta-exe@users.noreply.github.com>
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the focused diagnosis and regression direction. This is an automated hermes-sweeper review; the behavior has already landed through the consolidated salvage in #50018, which explicitly credits this PR.

  • d54890870ffd50a596b1ba0272bc05889e3e35c7 (fix(cron): make live-adapter delivery confirmation reliable (#38922, #47056, #43014)) implements the Cron delivery: confirmation timeout causes duplicate message (resend with no idempotency) #38922 timeout handling and is on current main.
  • cron/scheduler.py:1721-1762 treats an in-flight timeout (future.cancel() == False) as assume-delivered and skips the standalone resend, preventing the duplicate reported here.
  • cron/scheduler.py:1740-1752 preserves fallback when cancellation succeeds, covering the separate undispatched-timeout case.
  • tests/cron/test_scheduler.py:3198-3330 covers both no-duplicate and pre-dispatch-fallback contracts.
  • The fix is included in release v2026.7.1.

@teknium1 teknium1 closed this Jul 14, 2026
@teknium1 teknium1 added the sweeper:implemented-on-main Sweeper: behavior already present on current main label Jul 14, 2026
Gravezzz pushed a commit to Gravezzz/hermes-agent that referenced this pull request Jul 21, 2026
…arch#38922, NousResearch#47056, NousResearch#43014)

Consolidates three cron-delivery defects in cron/scheduler.py::_deliver_result
that all stem from how the live-adapter send result is interpreted.

NousResearch#38922 — duplicate message on confirmation timeout.
  future.result(timeout=60) raising TimeoutError bubbled to the outer
  except handler, which left delivered=False, so `if not delivered:` re-sent
  the identical message via the standalone path. future.cancel() cannot
  un-send a request already in flight on the wire, so a slow confirmation
  deterministically produced a duplicate. The send was already dispatched onto
  the gateway loop, so a bare timeout is now treated as delivered
  (assume-delivered is safer than guaranteed-duplicate) and the standalone
  fallback is skipped. The live-adapter media attempt is also skipped on
  timeout since the contended loop would re-block each 30s media budget.

NousResearch#47056 — silent drop when the gateway has an active session.
  The old check `if send_result is None or not getattr(send_result,
  "success", True)` let a result object missing a `success` attribute default
  to True = counted as a successful delivery, so the scheduler logged
  "delivered via live adapter" while the gateway never processed the message.
  Delivery is now confirmed via _confirm_adapter_delivery(): only an explicit,
  truthy `success` attribute counts; None or a `success`-less object falls
  through to the standalone path so the message actually arrives.

  A genuine send Exception (not a slow confirmation) still falls through to
  the standalone path, and is caught by run_job's outer handler — it is
  recorded as the job's last_error and never crashes the cron ticker.

NousResearch#43014 — deliver=origin fails to resolve in CLI sessions.
  A CLI-created job has no {platform, chat_id} origin, so deliver=origin (and
  auto-detect / deliver=None) was unresolvable and emitted "no delivery target
  resolved" on every run. An unresolvable origin with no configured home
  channel is now treated as local (output stays in last_output), matching the
  documented auto-deliver contract; a concrete unresolvable platform target
  still reports a real error.

Salvaged from NousResearch#41007 (timeout discriminator), folding in NousResearch#47127's
_confirm_adapter_delivery hardening and NousResearch#38937 / NousResearch#43063's origin→local
fallback. Tests rewritten as behavior contracts (timeout => no duplicate;
None / success-less result => standalone fallback; confirmed success => no
fallback; CLI origin => local, explicit platform => still errors).

Co-authored-by: Evi Nova <66773372+Tranquil-Flow@users.noreply.github.com>
Co-authored-by: kyssta-exe <kyssta-exe@users.noreply.github.com>
leewenjie pushed a commit to leewenjie/hermes-agent that referenced this pull request Aug 7, 2026
…arch#38922, NousResearch#47056, NousResearch#43014)

Consolidates three cron-delivery defects in cron/scheduler.py::_deliver_result
that all stem from how the live-adapter send result is interpreted.

NousResearch#38922 — duplicate message on confirmation timeout.
  future.result(timeout=60) raising TimeoutError bubbled to the outer
  except handler, which left delivered=False, so `if not delivered:` re-sent
  the identical message via the standalone path. future.cancel() cannot
  un-send a request already in flight on the wire, so a slow confirmation
  deterministically produced a duplicate. The send was already dispatched onto
  the gateway loop, so a bare timeout is now treated as delivered
  (assume-delivered is safer than guaranteed-duplicate) and the standalone
  fallback is skipped. The live-adapter media attempt is also skipped on
  timeout since the contended loop would re-block each 30s media budget.

NousResearch#47056 — silent drop when the gateway has an active session.
  The old check `if send_result is None or not getattr(send_result,
  "success", True)` let a result object missing a `success` attribute default
  to True = counted as a successful delivery, so the scheduler logged
  "delivered via live adapter" while the gateway never processed the message.
  Delivery is now confirmed via _confirm_adapter_delivery(): only an explicit,
  truthy `success` attribute counts; None or a `success`-less object falls
  through to the standalone path so the message actually arrives.

  A genuine send Exception (not a slow confirmation) still falls through to
  the standalone path, and is caught by run_job's outer handler — it is
  recorded as the job's last_error and never crashes the cron ticker.

NousResearch#43014 — deliver=origin fails to resolve in CLI sessions.
  A CLI-created job has no {platform, chat_id} origin, so deliver=origin (and
  auto-detect / deliver=None) was unresolvable and emitted "no delivery target
  resolved" on every run. An unresolvable origin with no configured home
  channel is now treated as local (output stays in last_output), matching the
  documented auto-deliver contract; a concrete unresolvable platform target
  still reports a real error.

Salvaged from NousResearch#41007 (timeout discriminator), folding in NousResearch#47127's
_confirm_adapter_delivery hardening and NousResearch#38937 / NousResearch#43063's origin→local
fallback. Tests rewritten as behavior contracts (timeout => no duplicate;
None / success-less result => standalone fallback; confirmed success => no
fallback; CLI origin => local, explicit platform => still errors).

Co-authored-by: Evi Nova <66773372+Tranquil-Flow@users.noreply.github.com>
Co-authored-by: kyssta-exe <kyssta-exe@users.noreply.github.com>
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 P2 Medium — degraded but workaround exists sweeper:implemented-on-main Sweeper: behavior already present on current main type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cron delivery: confirmation timeout causes duplicate message (resend with no idempotency)

3 participants