fix(cron): bound the desktop run-history query to one job (#41088)

The cron run-history endpoint (GET /api/cron/jobs/{id}/runs, added in
#40684) reused list_sessions_rich's order_by_last_active path with a
leading-wildcard id_query. That routes through the recursive
compression-chain CTE, which seeds from EVERY source='cron' row in the DB
and runs per-row preview/last_active subqueries before filtering to one
job and applying LIMIT. Work scaled with the total cron history, so a
large pile made the run-history load time out before eventually
populating.

Cron runs are flat, never-compressed sessions with ids of the form
cron_{job_id}_{ts}, so the chain machinery is pure overhead and the
job binding is a true prefix, not a substring.

- New SessionDB.list_cron_job_runs(): bounded [prefix, hi) id-range scan
  on source='cron', ordered by started_at DESC, with the same
  preview/last_active enrichment. No CTE, no leading-wildcard LIKE.
- Add idx_sessions_source(source, id) so the range is an index scan;
  bump SCHEMA_VERSION 14 -> 15 (index reconciles onto existing DBs via
  CREATE INDEX IF NOT EXISTS on startup).
- Point the endpoint at the new method.

Measured on a real SessionDB with 30k cron rows: 5ms vs 85ms for the old
path (16x), and the new path stays flat as the pile grows while the old
one scaled with it. Verified the query plan uses idx_sessions_source_id
(range scan, no full table scan), runs are correctly scoped (substring
collisions like cron_xalpha_ excluded), newest-first, and paged.
This commit is contained in:
Teknium 2026-06-07 02:41:01 -07:00 committed by GitHub
parent 5a3092b601
commit ed81cfe3de
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 178 additions and 9 deletions

View file

@ -5657,9 +5657,14 @@ async def list_cron_job_runs(job_id: str, profile: Optional[str] = None, limit:
Cron runs are stored as ordinary sessions whose id is
``cron_{job_id}_{timestamp}`` (see cron/scheduler.run_job). A job's history
is therefore every session whose id carries that prefix; ``source='cron'``
narrows it and the id substring binds it to this job. Powers the run-history
narrows it and the id prefix binds it to this job. Powers the run-history
list under each job in the desktop cron detail. Same row shape as
``/api/sessions`` so the frontend can reuse SessionInfo.
Backed by ``SessionDB.list_cron_job_runs`` a bounded ``[prefix, hi)``
id-range scan, not the compression-chain CTE used for the recents list,
so the cost scales with the requested window and not the (unbounded) total
cron history.
"""
selected = profile or _find_cron_job_profile(job_id)
# job_id may be a human name; resolve to the canonical id used in run-session ids.
@ -5676,13 +5681,7 @@ async def list_cron_job_runs(job_id: str, profile: Optional[str] = None, limit:
db = _open_session_db_for_profile(selected)
try:
runs = db.list_sessions_rich(
source="cron",
id_query=f"cron_{canonical}_",
limit=limit_n,
offset=0,
order_by_last_active=True,
)
runs = db.list_cron_job_runs(canonical, limit=limit_n, offset=0)
now = time.time()
for s in runs:
s["is_active"] = (