Skip to content

fix: show script-only cron run history in desktop - #61403

Open
LeonSGP43 wants to merge 1 commit into
NousResearch:mainfrom
LeonSGP43:codex/fix-61031-cron-runs-panel
Open

fix: show script-only cron run history in desktop#61403
LeonSGP43 wants to merge 1 commit into
NousResearch:mainfrom
LeonSGP43:codex/fix-61031-cron-runs-panel

Conversation

@LeonSGP43

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes the desktop cron runs history panel for --no-agent script-only jobs.

Before this change, /api/cron/jobs/{job_id}/runs only returned session-backed
history from SessionDB.list_cron_job_runs(). Script-only cron jobs do not
create 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>/*.md or from the job's own last_run_at metadata when
no 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

  • 🐛 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

  • Added cron run-history fallback logic in hermes_cli/web_server.py so
    /api/cron/jobs/{job_id}/runs can synthesize rows from cron output markdown
    files or job metadata when no session rows exist.
  • Updated apps/desktop/src/app/chat/sidebar/cron-jobs-section.tsx to render
    synthetic cron-output rows as non-clickable history entries.
  • Updated apps/desktop/src/app/cron/index.tsx with the same synthetic-row
    handling to avoid trying to open nonexistent sessions.
  • Added regression tests in tests/hermes_cli/test_web_server.py for:
    output-doc fallback, preserving session-backed rows when present, and
    metadata-only latest-run fallback.

How to Test

  1. Create or use a cron job configured with --no-agent so it produces output
    markdown files without creating Hermes sessions.
  2. Open the desktop cron jobs page or the chat sidebar cron runs panel for that
    job.
  3. Confirm prior runs are listed instead of "No runs yet", and that those rows
    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.py
  • uv run pytest tests/hermes_cli/test_web_server.py -q -k 'TestCronRunHistoryFallback'
  • npm --prefix apps/desktop run typecheck
  • git diff --check

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: macOS 26.4.1

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — or N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — or N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — or N/A
  • I've updated tool descriptions/schemas if I changed tool behavior — or N/A

Screenshots / Logs

Targeted regression proof:

$ uv run pytest tests/hermes_cli/test_web_server.py -q -k 'TestCronRunHistoryFallback'
...                                                                      [100%]
3 passed, 366 deselected in 0.37s

@alt-glitch alt-glitch added type/bug Something isn't working comp/desktop Electron desktop app (apps/desktop/*) comp/cli CLI entry point, hermes_cli/, setup wizard comp/cron Cron scheduler and job management P3 Low — cosmetic, nice to have labels Jul 9, 2026

@maxmilian maxmilian 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.

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 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 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:10207 attaches the server's current fixed-offset timezone to filename timestamps. cron/jobs.py:1921 writes 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 for started_at under a configured timezone mismatch and across DST.

Automated hermes-sweeper review.

Comment thread hermes_cli/web_server.py
naive = datetime.strptime(path.stem, _CRON_OUTPUT_FILENAME_FORMAT)
except ValueError:
return None
return naive.replace(tzinfo=datetime.now().astimezone().tzinfo).timestamp()

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.

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.

@andrexibiza

Copy link
Copy Markdown
Contributor

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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard comp/cron Cron scheduler and job management comp/desktop Electron desktop app (apps/desktop/*) P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Desktop cron runs panel shows "No runs yet" for --no-agent (script-only) jobs

5 participants