fix: show script-only cron run history in desktop - #61403
Conversation
maxmilian
left a comment
There was a problem hiding this comment.
Nice fix — the session-vs-output-doc split is exactly the right diagnosis, and I like that the metadata fallback degrades to a single row rather than showing nothing.
One correctness issue in _cron_output_run_timestamp, and it's invisible in the current tests because they only assert id / title / source, never started_at.
The output filename is written by save_job_output (cron/jobs.py:1921) using _hermes_now(), which is hermes_time.now() — the user's configured timezone (HERMES_TIMEZONE / config), falling back to server-local. This PR reads it back with datetime.now().astimezone().tzinfo, which is a fixed-offset snapshot of the server's local zone right now. Those diverge two ways.
1. Configured timezone ≠ server timezone. Running your _cron_output_run_timestamp unmodified on this branch, with TZ=UTC and HERMES_TIMEZONE=Asia/Taipei:
hermes_time.now() = 2026-07-10T00:14:10+08:00
filename written = 2026-07-10_00-14-10.md
PR parses back to = 2026-07-10T08:14:10+08:00
ERROR = 28799s = 8.0 hours
2. DST, even with no configured timezone. A fixed offset captured today gets applied to a filename recorded on the other side of a DST transition. With TZ=America/New_York, a January run viewed from July:
filename : 2026-01-15_09-00-00.md (written 09:00 EST)
true epoch : 2026-01-15T09:00:00-05:00
PR epoch : 2026-01-15T08:00:00-05:00
ERROR = -3600s
Both go away by attaching the zone rather than a snapshot offset, so DST resolves for the date in the filename instead of today's date:
from hermes_time import get_timezone
def _cron_output_run_timestamp(path: Path) -> Optional[float]:
try:
naive = datetime.strptime(path.stem, _CRON_OUTPUT_FILENAME_FORMAT)
except ValueError:
return None
tz = get_timezone()
if tz is not None:
return naive.replace(tzinfo=tz).timestamp()
# No configured zone: interpret as server-local wall time. astimezone() on a
# naive datetime picks the offset in effect on *that* date, so DST is correct.
return naive.astimezone().timestamp()I checked this against four cases — configured-tz ≠ server-tz, a DST-winter file, a DST-summer file, and a plain UTC server — and all four come back to err=0s.
For what it's worth, _cron_job_last_run_timestamp is already correct: last_run_at is stored as _hermes_now().isoformat() (cron/jobs.py:1390), so it carries an offset and fromisoformat().timestamp() round-trips fine. The bug is confined to the filename-derived path.
Might be worth asserting on started_at in test_falls_back_to_output_docs_when_no_session_runs_exist with HERMES_TIMEZONE monkeypatched to something other than the runner's zone — that would pin it.
teknium1
left a comment
There was a problem hiding this comment.
Thanks for addressing a real desktop history gap: current main only returns SessionDB-backed rows at hermes_cli/web_server.py:10218, while no_agent deliberately avoids SessionDB construction at cron/scheduler.py:2512.
Problems
hermes_cli/web_server.py:10207attaches the server's current fixed-offset timezone to filename timestamps.cron/jobs.py:1921writes filenames using_hermes_now(), which honors the configured IANA timezone. This displays the wrong instant when those zones differ and can be off by an hour for a historical file across DST.
Suggested changes
- Interpret filename wall time with
hermes_time.get_timezone()when configured; otherwise resolve it as local time on the filename's date. Add assertions forstarted_atunder a configured timezone mismatch and across DST.
Automated hermes-sweeper review.
| naive = datetime.strptime(path.stem, _CRON_OUTPUT_FILENAME_FORMAT) | ||
| except ValueError: | ||
| return None | ||
| return naive.replace(tzinfo=datetime.now().astimezone().tzinfo).timestamp() |
There was a problem hiding this comment.
save_job_output() writes this filename using _hermes_now() (cron/jobs.py:1921), which may be a configured IANA timezone. Attaching the server's current fixed offset changes the represented instant when those differ and is wrong for files across DST. Use hermes_time.get_timezone() when configured (or resolve local wall time for the filename's date) and add a started_at regression assertion.
|
Coordination note (dedup campaign): this PR is green-lit as the run-history half of #42433 (sweeper keep_open salvageability=high, and the output-doc vs session split diagnosis is right). Two things to resolve before merge: (1) branch is CONFLICTING against current main — needs rebase; (2) maxmilian's review flagged a filename-timestamp correctness issue in the output-doc fallback. The blank-detail half of #42433 is now covered by #77382, so this PR can stay scoped to history only. Also note it's the fix for #62341 (confirmed dup of #42433). |
What does this PR do?
Fixes the desktop cron runs history panel for
--no-agentscript-only jobs.Before this change,
/api/cron/jobs/{job_id}/runsonly returned session-backedhistory from
SessionDB.list_cron_job_runs(). Script-only cron jobs do notcreate sessions, so the desktop UI always showed "No runs yet" even when the
job had already produced markdown output files or had last-run metadata.
This patch adds a backend fallback that synthesizes history rows from
cron/output/<job_id>/*.mdor from the job's ownlast_run_atmetadata whenno session-backed rows exist. The desktop surfaces then render those synthetic
rows as read-only history entries instead of clickable session links.
Related Issue
Fixes #61031
Type of Change
Changes Made
hermes_cli/web_server.pyso/api/cron/jobs/{job_id}/runscan synthesize rows from cron output markdownfiles or job metadata when no session rows exist.
apps/desktop/src/app/chat/sidebar/cron-jobs-section.tsxto rendersynthetic cron-output rows as non-clickable history entries.
apps/desktop/src/app/cron/index.tsxwith the same synthetic-rowhandling to avoid trying to open nonexistent sessions.
tests/hermes_cli/test_web_server.pyfor:output-doc fallback, preserving session-backed rows when present, and
metadata-only latest-run fallback.
How to Test
--no-agentso it produces outputmarkdown files without creating Hermes sessions.
job.
render as read-only history entries rather than clickable session links.
Targeted proof run for this patch:
uv run python -m py_compile hermes_cli/web_server.py tests/hermes_cli/test_web_server.pyuv run pytest tests/hermes_cli/test_web_server.py -q -k 'TestCronRunHistoryFallback'npm --prefix apps/desktop run typecheckgit diff --checkChecklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests passDocumentation & Housekeeping
docs/, docstrings) — or N/Acli-config.yaml.exampleif I added/changed config keys — or N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — or N/AScreenshots / Logs
Targeted regression proof: