Skip to content

fix(cron): bind authoritative delivery target into cron worker turns (origin misroute) - #278

Merged
Kyzcreig merged 3 commits into
mainfrom
fix/cron-delivery-target-binding
Jul 11, 2026
Merged

Kyzcreig merged 3 commits into
mainfrom
fix/cron-delivery-target-binding

Conversation

@Kyzcreig

Copy link
Copy Markdown
Collaborator

Summary

Sibling of #271, cron path. A cron job with a CORRECT stored origin (deliver=origin, chat_id X) had its agent turn post a status message to a DIFFERENT Discord thread (live incident 2026-07-10 15:32, evidence in task t_37659d3c).

Root cause

Cron execution deliberately carries no HERMES_SESSION_* sender identity (delivery metadata lives in HERMES_CRON_AUTO_DELIVER_* ContextVars). But nothing surfaced the authoritative target to the WORKER TURN itself — so a cron turn that spawns helpers emitting ACKs/status inferred a delivery chat from task text / referenced work, and misrouted.

Fix

_bind_cron_delivery_target_hint(): the scheduler injects the job's stored delivery target (platform/chat_id/thread_id JSON, authoritative) into the cron worker prompt, with explicit instruction that nested status emitters route ONLY there and read the same target from the HERMES_CRON_AUTO_DELIVER_* env in shell commands — never inferring a target from task content.

Tests

  • RED to GREEN: new tests fail without the binding, pass with it
  • Mutation: removing the bind call goes red
  • 227 cron scheduler tests + 172 related gateway/send tests pass (py3.11)

Cache safety

Prompt-prefix injection happens at job-prompt ASSEMBLY (before the turn starts), not mid-conversation — no cache invalidation of live sessions.

Expose the resolved cron delivery target to worker prompts so nested ACK and heartbeat helpers do not infer a foreign chat from task context.\n\nVerified: scripts/run_tests.sh tests/cron/test_scheduler.py -q (227 passed); related gateway/send suites (172 passed); ruff check passed; mutation removal fails the new worker regression.
@Kyzcreig
Kyzcreig enabled auto-merge (squash) July 10, 2026 23:12
@greptile-apps

greptile-apps Bot commented Jul 10, 2026 •

Copy link
Copy Markdown

Greptile Summary

This PR fixes a cron misroute bug where a cron worker's nested subprocess emitted an ACK to a wrong Discord thread because the authoritative delivery target was never surfaced to the worker prompt. The fix injects the stored delivery target into the prompt before the agent turn starts, so model-generated helper processes cannot infer a foreign channel from task text.

  • New _bind_cron_delivery_target_hint() prepends a compact JSON routing directive to the worker prompt when a resolved delivery target exists; thread_id is always encoded as "" (not null) to match the HERMES_CRON_AUTO_DELIVER_THREAD_ID ContextVar representation exactly — addressing the representation divergence flagged in the previous review.
  • run_job change applies the hint after ContextVar bindings so both authoritative sources (HERMES_CRON_AUTO_DELIVER_* and the prompt JSON) are guaranteed to reflect the same resolved target.
  • Tests add a full run_job integration regression (FakeAgent captures prompt + ContextVars) and strengthen the transport test with a session-env contamination scenario.

Confidence Score: 5/5

Safe to merge — the change is confined to prompt assembly before the agent turn starts, does not touch delivery transport, and is fully covered by a new integration regression plus a strengthened transport test.

The fix is a single prompt-prefix injection at a well-defined point in run_job (after ContextVar bindings, before agent execution). The previous review concern about thread_id null vs. empty-string representation is explicitly addressed in a code comment and normalised to match the ContextVar convention. No delivery, gateway, or session-context code is modified. The mutation check described in the PR description directly validates that the new test catches the regression.

No files require special attention.

Important Files Changed

Filename Overview
cron/scheduler.py Adds _bind_cron_delivery_target_hint() and a single call in run_job after ContextVar bindings; thread_id null→empty-string normalisation is consistent with the ContextVar convention and explicitly noted in a comment.
tests/cron/test_scheduler.py Renames and strengthens test_origin_delivery_preserves_thread_id with session-env contamination, adds new run_job integration test that captures both the worker prompt and live ContextVar values.
PROGRESS.md New file documenting the incident diagnosis, hypotheses, root-cause analysis, and implementation/verification steps for task t_37659d3c; no production code impact.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Scheduler as cron/scheduler.py (run_job)
    participant BuildPrompt as _build_job_prompt()
    participant Scanner as _scan_assembled_cron_prompt()
    participant BindHint as _bind_cron_delivery_target_hint()
    participant ContextVar as HERMES_CRON_AUTO_DELIVER_* ContextVars
    participant Agent as AIAgent.run_conversation()
    participant Helper as Nested subprocess / helper

    Scheduler->>BuildPrompt: build prompt (skills, cron_hint)
    BuildPrompt->>Scanner: scan assembled prompt for injection
    Scanner-->>BuildPrompt: cleaned prompt
    BuildPrompt-->>Scheduler: assembled prompt
    Scheduler->>ContextVar: set PLATFORM / CHAT_ID / THREAD_ID from delivery_target
    Scheduler->>BindHint: bind delivery target hint (same delivery_target)
    Note over BindHint: thread_id encoded as "" (not null)
    BindHint-->>Scheduler: "[IMPORTANT: CRON DELIVERY TARGET {json}]...prompt"
    Scheduler->>Agent: run_conversation(final_prompt)
    Agent->>Helper: spawn subprocess
    Helper->>Helper: "read HERMES_CRON_AUTO_DELIVER_* env vars"
    Note over Helper: cannot infer chat from task text
    Helper-->>ContextVar: route ACK/heartbeat to authoritative target only
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Scheduler as cron/scheduler.py (run_job)
    participant BuildPrompt as _build_job_prompt()
    participant Scanner as _scan_assembled_cron_prompt()
    participant BindHint as _bind_cron_delivery_target_hint()
    participant ContextVar as HERMES_CRON_AUTO_DELIVER_* ContextVars
    participant Agent as AIAgent.run_conversation()
    participant Helper as Nested subprocess / helper

    Scheduler->>BuildPrompt: build prompt (skills, cron_hint)
    BuildPrompt->>Scanner: scan assembled prompt for injection
    Scanner-->>BuildPrompt: cleaned prompt
    BuildPrompt-->>Scheduler: assembled prompt
    Scheduler->>ContextVar: set PLATFORM / CHAT_ID / THREAD_ID from delivery_target
    Scheduler->>BindHint: bind delivery target hint (same delivery_target)
    Note over BindHint: thread_id encoded as "" (not null)
    BindHint-->>Scheduler: "[IMPORTANT: CRON DELIVERY TARGET {json}]...prompt"
    Scheduler->>Agent: run_conversation(final_prompt)
    Agent->>Helper: spawn subprocess
    Helper->>Helper: "read HERMES_CRON_AUTO_DELIVER_* env vars"
    Note over Helper: cannot infer chat from task text
    Helper-->>ContextVar: route ACK/heartbeat to authoritative target only
Loading

Reviews (3): Last reviewed commit: "Merge branch 'main' into fix/cron-delive..." | Re-trigger Greptile

Comment thread cron/scheduler.py Outdated
@Kyzcreig
Kyzcreig merged commit 893cd9b into main Jul 11, 2026
35 checks passed
@Kyzcreig
Kyzcreig deleted the fix/cron-delivery-target-binding branch July 11, 2026 00:23
Kyzcreig added a commit that referenced this pull request Jul 26, 2026
…00 summary-role pin) (#431)

* vendor(lcm): cherry-pick 9 upstream fixes from hermes-lcm (03b74f8 -> selected from 49e99a2)

Upstream hermes-lcm went MIT (LICENSE added 2026-06-26, f7ae61f) — vendoring
and cherry-picking now permitted with attribution. Tier-1 picks per
/tmp/lcm-refresh-out/CHERRY-PICK-LIST.md, code-only (their tests/docs/changelog
excluded; our vendored copy carries no tests dir — coverage rides
tests/context_engine/):

  #263 preserve source lineage after long sessions
  #264 perf: aggregate DAG status stats
  #265 harden externalized payload durability
  #269 preserve raw session ownership across compression rollover
  #278 avoid payload integrity false positives from log examples
  #280 pin summary role to user after system anchor (Anthropic HTTP 400) <- highest value
  #285 make context engine deepcopy clone-safe (subagent spawn safety)
  #282 strip injected context before compaction
  (+) discard reasoning-only summaries (unclosed <think> = quality bug + prompt leak)

All 9 verified clean-apply by the refresh-analysis worker on a simulated copy
of our tree; re-applied here onto fork/main.

* test(compaction): regenerate in-turn reconcile fixture for the vendored sanitizer

The 9 LCM cherry-picks (3c1d61c) add one line to
_sanitize_active_context_messages (upstream pick #282, strip injected
context before compaction), which moves the fixture's
sanitizer_source_sha1 provenance hash and reds
test_fixture_sanitizer_provenance_current.

Regenerated via the committed generator:
  python tests/agent/fixtures/gen_inturn_reconcile_fixture.py

Diff is the provenance hash ONLY -- messages, compressed,
true_kept_count and fresh_tail_count are byte-identical. The new line
routes through _preserved_objective_context_content, which returns ""
unless a row starts with the preserved-objective prefix, so it is a
strict no-op on all 632 fixture rows (verified: 0 rows mutated) and the
real sanitizer output is unchanged. Only one of the four hashed
functions changed; the other three are byte-identical.

Follow-up candidate (not this PR): this provenance test is a
change-detector, which AGENTS.md discourages -- it should assert the
sanitizer's behaviour (live sanitize(raw_tail) == committed comp tail)
rather than pinning its source SHA.

---------

Co-authored-by: Kyzcreig <9063726+Kyzcreig@users.noreply.github.com>
Kyzcreig added a commit that referenced this pull request Sep 25, 2026
…009b, batch 3)

#278 and lifecycle-guard bundles (#319+#594, ssh trio, #920+#933+#1017, #740, #767) re-ported onto upstream/main.
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.

1 participant