Skip to content

fix(cron): cap session_id at 64 chars to satisfy prompt_cache_key limit - #28307

Closed
wsh123098-lang wants to merge 1 commit into
NousResearch:mainfrom
wsh123098-lang:wsh/cron-prompt-cache-key-length-fix
Closed

fix(cron): cap session_id at 64 chars to satisfy prompt_cache_key limit#28307
wsh123098-lang wants to merge 1 commit into
NousResearch:mainfrom
wsh123098-lang:wsh/cron-prompt-cache-key-length-fix

Conversation

@wsh123098-lang

Copy link
Copy Markdown

What does this PR do?

Cron jobs with a job_id longer than 43 characters fail at first fire with HTTP 400 from OpenAI / xAI:

RuntimeError: Error code: 400 - {'error': {'message': "Invalid 'prompt_cache_key': string too long. Expected a string with maximum length 64, but got a string with length 68 instead.", 'type': 'invalid_request_error', 'param': 'prompt_cache_key', 'code': 'string_above_max_length'}}

Root cause is in cron/scheduler.py: the session id passed to run_agent(...) (and from there to prompt_cache_key on the Responses API) is built as

_cron_session_id = f"cron_{job_id}_{_hermes_now().strftime('%Y%m%d_%H%M%S')}"

The fixed cron_ + _YYYYMMDD_HHMMSS overhead is 21 chars, leaving only 43 chars for job_id before the assembled key blows past the API's 64-char prompt_cache_key cap. There is no length validation when jobs are created, and no defensive truncation at the transport layer (agent/transports/codex.py passes the session id straight through), so a single long-named oneshot is enough to silently break.

This PR truncates over-long job_ids when building the cache key, appending an 8-char sha1 suffix so two long names that share a prefix don't collide on the same cache scope. Short job_ids (≤ 43 chars) are unchanged — there is no behavior change for any existing cron job whose id fits.

Related Issue

No existing issue — I searched prompt_cache_key, session_id length, and cron 400 against the open issue list and found nothing matching.

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

  • cron/scheduler.py:
    • Add _safe_job_id_for_session(job_id) helper near _resolve_origin that truncates long job_ids to 64 - len("cron__YYYYMMDD_HHMMSS") = 43 chars and appends an 8-char sha1 digest for collision resistance.
    • Use it when building _cron_session_id at the existing call site.
    • Add import hashlib.
  • tests/cron/test_scheduler.py:
    • New TestSafeJobIdForSession class with three regression tests:
      • Short id is passed through unchanged (including the 43-char boundary).
      • The originally tripping job_id (oneshot-plan-b-merge-transition-into-evaluation, 47 chars) yields a 43-char safe id and a final session id of exactly 64 chars.
      • Two long job_ids that share the truncation prefix don't collide.

How to Test

Reproduce the bug on main:

  1. Create a oneshot whose id is > 43 chars (e.g. via hermes cron create with a long --name, or any job whose auto-generated id ends up long).
  2. Let it fire. The job fails with the 400 above and never runs.

Verify the fix on this branch:

  1. Same setup. The job fires successfully; prompt_cache_key is now ≤ 64 chars.
  2. Run the new regression tests:
    pytest tests/cron/test_scheduler.py -k SafeJobId -v
    
  3. Full cron suite still passes:
    pytest tests/cron/ -q   # 370 passed
    

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(cron): ...)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix (no unrelated commits)
  • I've run pytest tests/cron/ -q and pytest tests/cron/test_scheduler.py -k SafeJobId -v and all relevant tests pass. The broader pytest tests/ -q run on my machine surfaces 5 pre-existing failures in tests/gateway/test_discord_*, tests/agent/test_anthropic_adapter.py, and tests/acp/test_edit_approval.py that are unrelated to this change (they reproduce on HEAD~1 without these edits; the acp failure is an env-specific /private/var/folders sensitive-path refusal).
  • I've added tests for my changes (3 new regression tests in TestSafeJobIdForSession)
  • I've tested on my platform: macOS 15 (Darwin 25.3.0), Python 3.11

Documentation & Housekeeping

  • I've updated relevant documentation — N/A (helper is private; behavior change is invisible to anyone whose job_ids already fit)
  • I've updated cli-config.yaml.example if I added/changed config keys — N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — N/A
  • I've considered cross-platform impact — N/A (pure stdlib, sha1 + slicing)
  • I've updated tool descriptions/schemas if I changed tool behavior — N/A

Screenshots / Logs

The error as observed in the wild (cron output dump):

Cron job 'oneshot-<elided-long-id>' failed:
RuntimeError: Error code: 400 - {'error': {'message': "Invalid 'prompt_cache_key': string too long. Expected a string with maximum length 64, but got a string with length 68 instead.", 'type': 'invalid_request_error', 'param': 'prompt_cache_key', 'code': 'string_above_max_length'}}

After the fix, the corresponding prompt_cache_key is 64 chars exactly:

cron_<34-char-prefix>-<8-char-sha1>_20260519_093057

OpenAI/xAI reject prompt_cache_key > 64 chars with HTTP 400. The cron
session_id format `cron_{job_id}_{YYYYMMDD_HHMMSS}` has 21 chars of
overhead, so any job_id over 43 chars (e.g. the oneshot
'oneshot-plan-b-merge-transition-into-evaluation' at 47) blew the limit
and the job failed to run.

Truncate long job_ids to fit and append an 8-char sha1 suffix so two
truncated names with a shared prefix don't collide on the cache scope.
Short job_ids (≤43 chars) pass through unchanged.
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/cron Cron scheduler and job management labels May 19, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Duplicate of #24273 (and #26468). All three fix prompt_cache_key exceeding the 64-char backend limit — #24273 fixes it at the codex transport layer, this PR fixes it at the cron scheduler layer. The transport-level fix in #24273 would cover this case too.

@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 the focused regression fix. The premise still holds on current main: cron builds cron_{job_id}_{timestamp} without a cap in cron/scheduler.py:1487, passes it as session_id at cron/scheduler.py:1755, and the Codex transport forwards that value to prompt_cache_key in agent/transports/codex.py:159 / agent/transports/codex.py:252.

Problems

  • The scheduler-side truncation needs to update the cron run-history contract too. Current main looks up a job's runs with prefix = f"cron_{job_id}_" in hermes_state.py:2238, and the web endpoint documents the same literal cron_{job_id}_{timestamp} shape in hermes_cli/web_server.py:6827-6835. After this PR, long job ids would run, but history lookup by the original job id would miss the truncated/hash-prefixed session ids.

Suggested changes

  • Reuse the same safe-id mapping in the run-history lookup, or persist an explicit cron job id on the session so lookup does not depend on the prompt-cache-safe session id string.
  • Add a regression test for a long job id that verifies both the capped session id and list_cron_job_runs(original_job_id) returning that run.

This is an automated hermes-sweeper review.

Comment thread cron/scheduler.py
@@ -1285,7 +1302,10 @@ def _run_job_impl(job: dict) -> tuple[bool, str, str, Optional[str]]:
logger.info("Job '%s': script produced no output, skipping AI call.", job_name)
return True, "", SILENT_MARKER, None

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 keeps the prompt_cache_key under the provider limit, but it also changes the session-id prefix for long job ids. SessionDB.list_cron_job_runs() still queries cron_{original_job_id}_, so long-job runs will become undiscoverable in the cron run-history path unless that lookup uses the same mapping or an explicit job-id field.

@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the focused cron regression fix. This is an automated hermes-sweeper review: current main already provides the reported bounded prompt_cache_key behavior through a stronger transport-level implementation.

  • agent/transports/codex.py:16-46 derives a fixed 28-character pck_ SHA-256 content key from the static request prefix.
  • agent/transports/codex.py:132-139 always supplies non-empty instructions before key construction; agent/transports/codex.py:260-271 therefore sends the bounded content key rather than the cron session ID.
  • tests/agent/transports/test_codex_transport.py:96-123 covers cron-shaped session IDs and verifies timestamp-distinct fires share the same cache key.
  • This landed in 7a65800fed6f7b3f87c264018b7456b250e150ac (fix(cache): content-address prompt_cache_key so recurring cron jobs reuse the warm prefix, fix(cache): content-address prompt_cache_key so recurring cron jobs reuse the warm prefix #52295), shipped in v2026.7.1.

This also avoids the scheduler-side session-id rewrite proposed here, preserving the existing cron_{job_id}_ run-history contract.

@teknium1 teknium1 closed this Jul 13, 2026
@teknium1 teknium1 added the sweeper:implemented-on-main Sweeper: behavior already present on current main label Jul 13, 2026
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.

3 participants