fix(cron): make cron_run non-blocking, enqueue async execution (#3127)#3142
Conversation
|
Warning Review limit reached
More reviews will be available in 31 minutes and 56 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (15)
📝 WalkthroughWalkthroughcron_run now enqueues jobs and returns {job_id, status: "queued"} immediately; a per-job in-process guard prevents duplicate concurrent runs; the frontend polls for run completion when queued, adds mount-safety, new i18n keys, and tests validate input, queueing, and concurrency rejection. ChangesAsync Cron Run with Queued Response
Sequence DiagramsequenceDiagram
participant Client
participant CronRunRPC as cron_run RPC
participant ACTIVE_RUNS
participant DB as cron_runs store
participant Background as Background Task
Client->>CronRunRPC: cron_run(job_id)
CronRunRPC->>ACTIVE_RUNS: check/insert job_id (acquire guard)
alt guard acquired
CronRunRPC->>DB: insert queued placeholder run (status="queued")
CronRunRPC->>Background: spawn background task to execute job
CronRunRPC-->>Client: return { job_id, status: "queued" }
Client->>DB: poll openhumanCronRuns(job_id, limit)
Background->>Background: execute_job_now()
Background->>DB: delete queued placeholder runs for job_id
Background->>DB: insert final run (status/duration/output)
Background->>ACTIVE_RUNS: remove job_id (drop guard)
else guard held
ACTIVE_RUNS-->>CronRunRPC: error "already running"
CronRunRPC-->>Client: return error
end
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested Reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 3❌ Failed checks (3 warnings)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
src/openhuman/cron/ops.rs (1)
196-255: ⚡ Quick winAdd debug/trace logs around enqueue, duplicate reject, and completion.
This work now continues after the RPC response is gone, so
RpcOutcomelogs alone are not enough to diagnose what happened later. A few process-level logs with a stable prefix andjob_idwould make queued runs and stuck/failed executions much easier to trace.As per coding guidelines, "In Rust, default to verbose diagnostics on new/changed flows using
log/tracingatdebug/tracelevels with stable grep-friendly prefixes and correlation fields".🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/openhuman/cron/ops.rs` around lines 196 - 255, Add trace/debug logging around the enqueue/duplicate-check and the spawned task lifecycle: log when rejecting a duplicate run (use ACTIVE_RUNS and job_id), log right after successful enqueue (before returning RpcOutcome), log at task start (inside the tokio::spawn block using started_at and job.id), and log on completion/failure including finished_at, duration_ms, status, and any errors from cron::record_run / cron::record_last_run / cron::scheduler::deliver_job; reference ACTIVE_RUNS, cron::get_job, tokio::spawn, cron::record_run, cron::record_last_run, cron::scheduler::deliver_job and include a stable grep-friendly prefix and job_id/correlation field in each message.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/src/pages/Routines.tsx`:
- Around line 85-105: The polling loop in handleRunNow should detect a new run
by comparing the newest run's id, not list length, and should stop updating
after timeout or when the component unmounts; change the logic to record
previousTopId = (runsByJob[jobId] ?? [])[0]?.id, then in each poll call
openhumanCronRuns(jobId, 10), setRunsByJob with runs.result, and break when
runs.result[0]?.id !== previousTopId (or exists and is greater); implement a
cancellation guard (e.g., an isMounted flag or AbortController) so you stop
polling and avoid setState after unmount, and add a timeout branch that surfaces
user feedback (e.g., set a local error/timeout state) when MAX_WAIT_MS is
exceeded.
In `@src/openhuman/cron/ops_tests.rs`:
- Around line 463-485: The test currently spawns a real long-running shell job
via add_shell_job(..., "sleep 60") and relies on timing to keep the job in
ACTIVE_RUNS; instead, seed ACTIVE_RUNS directly so the second cron_run(&config,
&job.id) is rejected deterministically. Modify the test to create the job
without relying on a long subprocess (e.g., still use add_shell_job or a
lightweight job creation helper to obtain job.id), then insert job.id into the
ACTIVE_RUNS guard/map used by the module (referencing the ACTIVE_RUNS symbol)
before calling cron_run the second time; assert the error contains "already
running" as before and remove the dependency on the shell sleep command and
timing.
In `@src/openhuman/cron/ops.rs`:
- Around line 220-255: Create a persistent "queued" run record before returning
so the frontend can observe the run; specifically, before tokio::spawn or before
returning RpcOutcome, call cron::record_run(&config_owned, &job.id, started_at,
started_at, "queued", None, 0) using let started_at = chrono::Utc::now() (and
set finished_at = started_at and duration_ms = 0), and ensure ACTIVE_RUNS is
updated to include job_id_owned so the run is discoverable while the background
task runs; keep the existing background task that later updates the run record
when it finishes.
- Around line 222-246: The spawned task can panic/abort and never reach the
explicit active.remove(&job_id_owned) call, so make the cleanup unconditional by
introducing a drop-guard that removes the job id from ACTIVE_RUNS in its Drop
impl; create e.g. an ActiveRunGuard that holds job_id_owned (and a reference to
ACTIVE_RUNS if needed) and performs the removal in Drop, instantiate it at the
start of the tokio::spawn async block (before any await points), and then remove
the explicit active.remove(&job_id_owned) at the end of the block—this ensures
cleanup runs on normal completion, panic, or cancellation while keeping the same
behavior around ACTIVE_RUNS and job_id_owned.
---
Nitpick comments:
In `@src/openhuman/cron/ops.rs`:
- Around line 196-255: Add trace/debug logging around the
enqueue/duplicate-check and the spawned task lifecycle: log when rejecting a
duplicate run (use ACTIVE_RUNS and job_id), log right after successful enqueue
(before returning RpcOutcome), log at task start (inside the tokio::spawn block
using started_at and job.id), and log on completion/failure including
finished_at, duration_ms, status, and any errors from cron::record_run /
cron::record_last_run / cron::scheduler::deliver_job; reference ACTIVE_RUNS,
cron::get_job, tokio::spawn, cron::record_run, cron::record_last_run,
cron::scheduler::deliver_job and include a stable grep-friendly prefix and
job_id/correlation field in each message.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 973780da-f404-4406-bc42-d36252386d23
📒 Files selected for processing (4)
app/src/pages/Routines.tsxapp/src/utils/tauriCommands/cron.tssrc/openhuman/cron/ops.rssrc/openhuman/cron/ops_tests.rs
There was a problem hiding this comment.
♻️ Duplicate comments (1)
app/src/pages/Routines.tsx (1)
94-102:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAdd cancellation + timeout feedback to the queued polling loop.
The loop can still update state after unmount, and on
MAX_WAIT_MSexpiry it exits silently. Please stop polling when unmounted and surface a timeout error for the user.Suggested patch
-import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; @@ const [runsByJob, setRunsByJob] = useState<Record<string, CoreCronRun[]>>({}); const [busyKeys, setBusyKeys] = useState<Set<string>>(new Set()); + const isMountedRef = useRef(true); + + useEffect(() => { + return () => { + isMountedRef.current = false; + }; + }, []); @@ while (elapsed < MAX_WAIT_MS) { await new Promise<void>(resolve => setTimeout(resolve, POLL_INTERVAL_MS)); + if (!isMountedRef.current) return; elapsed += POLL_INTERVAL_MS; const runs = await openhumanCronRuns(jobId, 10); + if (!isMountedRef.current) return; setRunsByJob(prev => ({ ...prev, [jobId]: runs.result })); if (runs.result[0]?.id !== undefined && runs.result[0].id !== previousLatestId) { break; } } + if (elapsed >= MAX_WAIT_MS && isMountedRef.current) { + setError(t('routines.runNowTimedOut')); + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/pages/Routines.tsx` around lines 94 - 102, The polling loop can continue after the component unmounts and currently exits silently on timeout; to fix this, add cancellation and explicit timeout state: in the effect/handler that runs the loop introduce a cancel flag or AbortController (e.g., let canceled = false or const ac = new AbortController()) and return a cleanup that sets canceled = true or ac.abort(); inside the while loop check if (canceled) break and avoid calling setRunsByJob when canceled; if openhumanCronRuns supports an abort signal pass ac.signal to it so in-flight requests are canceled; when elapsed >= MAX_WAIT_MS (timeout) set a user-visible error state (e.g., create/setPollingError or setPollingTimeout keyed by jobId) with a descriptive message like "Timed out waiting for new run" so the UI can show feedback; ensure previousLatestId and runs.result checks still guard normal break behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Duplicate comments:
In `@app/src/pages/Routines.tsx`:
- Around line 94-102: The polling loop can continue after the component unmounts
and currently exits silently on timeout; to fix this, add cancellation and
explicit timeout state: in the effect/handler that runs the loop introduce a
cancel flag or AbortController (e.g., let canceled = false or const ac = new
AbortController()) and return a cleanup that sets canceled = true or ac.abort();
inside the while loop check if (canceled) break and avoid calling setRunsByJob
when canceled; if openhumanCronRuns supports an abort signal pass ac.signal to
it so in-flight requests are canceled; when elapsed >= MAX_WAIT_MS (timeout) set
a user-visible error state (e.g., create/setPollingError or setPollingTimeout
keyed by jobId) with a descriptive message like "Timed out waiting for new run"
so the UI can show feedback; ensure previousLatestId and runs.result checks
still guard normal break behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 89cad467-4c16-4b06-b3ae-f7b95aa5aadf
📒 Files selected for processing (3)
app/src/pages/Routines.tsxsrc/openhuman/cron/ops.rssrc/openhuman/cron/ops_tests.rs
🚧 Files skipped from review as they are similar to previous changes (2)
- src/openhuman/cron/ops_tests.rs
- src/openhuman/cron/ops.rs
graycyrus
left a comment
There was a problem hiding this comment.
@YellowSnnowmann CI is failing (Frontend Coverage, Rust Core Coverage, Rust E2E, Playwright lane 1/4), so i can't approve yet. i did a full pass and found two issues worth fixing alongside the CI failures:
-
The queued placeholder record causes the frontend poller to exit after ~2 seconds, not after the job actually finishes — see inline on ops.rs. The spinner disappears prematurely for any job that takes more than 2 seconds.
-
No unmount guard in the polling loop — if the user navigates away mid-poll, the loop keeps firing network requests and calling setRunsByJob on an unmounted component. CodeRabbit flagged this (isMounted ref or AbortController) but the fix isn't in the current diff despite being marked resolved.
Apart from those two: the RAII guard design is correct, the deterministic test is the right approach, and the overall architecture of the async refactor is solid. Fix CI and these two, and i'll come back to approve.
graycyrus
left a comment
There was a problem hiding this comment.
@YellowSnnowmann the three items from my last pass are all addressed — status-based break condition, unmount guard, and the isMountedRef guards on both await points are in. Good work turning those around quickly.
One new issue surfaced:
record_run is a plain INSERT (no update/upsert). The backend calls it twice per "Run Now": once for the "queued" placeholder before spawning, and once inside the spawned task with the terminal status. The placeholder row is never cleaned up, so every "Run Now" click leaves a permanent orphaned row in cron_runs with status = 'queued', duration_ms = 0, and null output. With max_run_history = N, the effective visible real-run count drops to roughly N/2 before pruning kicks in — and the history view will show ghost "queued" rows mixed in with real results.
Two clean options:
- Option A — drop the placeholder entirely. The break condition already handles this: without a placeholder,
previousLatestIdstays correct and the loop waits until the real terminal record appears. This is the simpler fix. - Option B — return the inserted row ID from the first
record_runcall and pass it into the spawned task, which then does an UPDATE on that row instead of a second INSERT. Keeps the observable "queued" state in history but avoids the orphan.
Option A is simpler and avoids any schema or store changes.
Holding off on approving until CI is green and the orphaned placeholder is addressed.
| let queued_at = chrono::Utc::now(); | ||
| let _ = cron::record_run(config, &job.id, queued_at, queued_at, "queued", None, 0); | ||
|
|
||
| let config_owned = config.clone(); |
There was a problem hiding this comment.
[major] record_run always does a plain INSERT — there is no update path. This call creates a "queued" row that persists permanently. The spawned task then calls record_run again with the terminal status, adding a second row. Every "Run Now" click produces two history entries, one of which is a ghost with status = 'queued', duration_ms = 0, and no output.
With max_run_history = N, roughly half of the slots are consumed by these ghost rows, and the UI will show them mixed in with real results.
Fix options:
- Drop this placeholder entirely — the
status !== 'queued'break condition in the frontend handles this correctly with no placeholder present. - Return the row ID from this call and have the spawned task UPDATE it instead of inserting a second row.
b6c79cf to
040bace
Compare
graycyrus
left a comment
There was a problem hiding this comment.
Continuation — two issues remain blocking merge.
First, the orphaned placeholder from my last review (thread on ops.rs:240) is still open. Every "Run Now" click calls record_run twice: once before tokio::spawn with status='queued', and once inside the spawned task with the terminal status. Since record_run is a plain INSERT with no update path, both rows persist. The ghost status='queued' row (duration_ms=0, output=null) is never cleaned up and pollutes run history indefinitely. Fix: either drop the placeholder entirely — the latest.status !== 'queued' break condition on the frontend handles a no-placeholder world correctly — or return the inserted row ID from the first call and have the background task UPDATE it rather than INSERT a second time.
Second, the memoryGraphLayout.test.ts changes (introduced in commit e8e36b8 but not called out until now). See inline comment.
The notification test waitFor fix in the latest commit is correct.
| }); | ||
|
|
||
| it('nodeRadius grows with summary level and is fixed for chunk/contact', () => { | ||
| it('nodeRadius scales with level and is fixed for chunk/contact', () => { |
There was a problem hiding this comment.
[major] These test expectations were updated with no corresponding change to memoryGraphLayout.ts anywhere in this PR. The behavior inverted: nodeRadius previously shrank from 10 (level 0) down to 4 (level 99), bounded — now it grows to 252.5 at level 99, unbounded. A 252.5px radius node would be 505px in diameter, which breaks any graph layout at higher summary levels.
This looks like the implementation was changed in an earlier commit outside this PR, CI broke, and these expectations were rubber-stamped to pass. That's the wrong fix — it hides a regression rather than addressing it.
Please either: (a) revert these test changes and investigate why memoryGraphLayout.ts changed behavior (and fix the implementation), or (b) explain in the PR what the intentional change to the radius formula is and why unbounded growth at high levels is correct. This is unrelated to the cron fix and shouldn't be bundled in here.
…umansai#3127) Replace the synchronous execute_job_now().await in the cron_run RPC handler with a tokio::spawn fire-and-forget. The handler now returns {status: "queued"} immediately, eliminating the 30 s RPC timeout. - Add ACTIVE_RUNS guard to prevent duplicate concurrent executions - Frontend polls openhumanCronRuns every 2 s (max 120 s) for completion - Update openhumanCronRun return type to include 'queued' status - Add ops_tests: empty/whitespace id, disabled cron, queued response, duplicate concurrent rejection Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add ActiveRunGuard (RAII drop guard) so ACTIVE_RUNS cleanup is panic/cancellation-safe, replacing the explicit remove at the end of the spawned task - Insert a "queued" placeholder run record before returning so the frontend poller can observe the run immediately without waiting for execute_job_now to finish - Add tracing::debug logs around enqueue, duplicate reject, task start, and task completion for diagnosability of async background runs - Fix Routines.tsx polling to compare newest run id (not list length), fixing the stuck-at-120s bug when a job already has >= 10 runs - Replace sleep 60 subprocess in duplicate-run test with direct ACTIVE_RUNS seeding for a deterministic, platform-independent test Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…t feedback Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
I see my prior CHANGES_REQUESTED review noted two critical issues that remain unaddressed: 1. Orphaned placeholder rows (src/openhuman/cron/ops.rs:382) 2. memoryGraphLayout test expectations (app/src/components/intelligence/memoryGraphLayout.test.ts:44) The latest commits (aa52bdb, 040bace) are test-only and don't touch the ops.rs placeholder logic or the memoryGraphLayout implementation. The core issues persist. Also, CI is still failing (Playwright E2E, Frontend Quality). These blocks need to be resolved before this can land. I'll do a full re-review once those are fixed. |
graycyrus
left a comment
There was a problem hiding this comment.
@YellowSnnowmann heads up — CI is still failing across multiple checks (Frontend typecheck, Rust Core Coverage, Playwright E2E, Build artifact), so I'm holding off on a full re-review until those are sorted.
More importantly though, your recent commits (4–5) are test-only fixes and don't address the two major findings from my prior REQUEST_CHANGES:
-
Orphaned placeholder rows —
ops.rs:240inserts a "queued" run record before spawning the background task, then the task inserts another record with the terminal status. Every "Run Now" click creates 2 rows, the orphaned "queued" row is never cleaned up, and it permanently pollutes the run history. Need to either UPSERT the placeholder (so there's only 1 row) or delete it after recording the final status. -
memoryGraphLayout test expectations rubber-stamped — The nodeRadius expectations were flipped from bounded to unbounded with no matching change to the implementation file. Looks like the test was updated to pass CI rather than fixing the actual formula. The 252.5px radius breaks layout at high summary levels.
Please fix these two issues first, then I'll re-review once CI is green. Thanks!
040bace to
f105f70
Compare
There was a problem hiding this comment.
@YellowSnnowmann heads up — CI is still pending on this PR (Rust E2E, Frontend Coverage, Build Playwright E2E, and coverage checks), so I'll hold off on final approval until those are sorted out.
I also found one major issue that needs addressing:
Orphaned "queued" placeholder rows (ops.rs:240) — the placeholder INSERT before tokio::spawn is never cleaned up or updated, so every "Run Now" creates 2 rows in the history. This pollutes the run log and halves the effective history slot capacity. Fix: UPSERT the placeholder on the terminal INSERT (update on conflict), or delete it after recording the final status.
Get CI green + fix this issue, then I'll come back and approve. Let me know if you need any help!
graycyrus
left a comment
There was a problem hiding this comment.
The polling logic looks solid now — you've fixed the frontend-side issues (unmount guard, status-based termination, id-based comparison), and the i18n/test coverage is good.
However, the core blocking issue remains unaddressed: the orphaned "queued" placeholder rows in ops.rs.
The problem: You insert a "queued" placeholder record (line 241) before spawning, then the background task calls record_run again with the terminal status. Because record_run is a plain INSERT (no UPSERT), each "Run Now" click produces two rows:
- Row 1: status='queued', duration_ms=0, output=NULL (inserted before spawn)
- Row 2: status='ok'/'error', duration_ms=actual, output=result (inserted in background task)
The first row is never cleaned up or updated, so the run history gets polluted with ghost "queued" rows. This halves your effective run history slots.
The fix: Either:
- UPSERT the placeholder — Make the second
record_runcall an UPSERT (update on conflict with id) so it updates the queued row to the terminal status instead of creating a new row. This gives you 1 row per run. - Delete the placeholder — After the terminal
record_runcompletes, delete the queued placeholder row. Same end result: 1 row per run.
This is blocking approval. CI is also failing (Playwright E2E, Rust Core Coverage), so there are two gates to clear:
- Fix the orphaned placeholder logic in ops.rs
- Green CI — sort out the E2E/coverage failures
Once both are done, I'll run a fresh review and we're good to merge.
Let me know if you need clarification on the orphaned row issue or the fix approach.
… assertion Delete "queued" placeholder records before inserting the real run result so that "Run Now" no longer leaves orphaned rows in cron_runs history. Revert .toBeCloseTo(12.5) back to .toBe(12.5) since nodeRadius(level=3) produces an exact value (5 + 3 * 2.5 = 12.5). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
graycyrus
left a comment
There was a problem hiding this comment.
@YellowSnnowmann hey! the code looks good to me now — the blocking issue (orphaned queued placeholder rows) is fixed via delete_queued_runs(), frontend polling logic is correct with the unmount guard, and test coverage is adequate. however, CI is still failing (E2E lane 1/4 + several pending jobs), so i'm holding off on approval until those are green. once the CI suite passes completely, i'll come back and approve. let me know if you need help debugging the failures.
sanil-23
left a comment
There was a problem hiding this comment.
I see @graycyrus has requested changes — deferring to their feedback. Will review once those are addressed.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/src/pages/Routines.tsx (1)
127-138:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winGuard
handleRunNowstate updates (and busy cleanup) against unmount
handleRunNowstill callssetJobs(...)(afteropenhumanCronList()), andremoveBusy(key)infinallywithout checkingisMountedRef.current—so both can trigger state updates after navigation/unmount. Guard those calls (andsetError(message)in thecatch) with the sameisMountedRef.currentpattern you use inside the polling loop.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/src/pages/Routines.tsx` around lines 127 - 138, The handleRunNow flow currently updates state after awaiting openhumanCronList() and in catch/finally without checking component mount status; wrap all post-await state setters (setJobs(...), setError(message)) and the cleanup call removeBusy(key) in an isMountedRef.current guard similar to the polling loop so they only run if isMountedRef.current is true; locate handleRunNow and add conditional checks around the setJobs call after sorting the response, around setError in the catch block, and around removeBusy(key) in the finally block to prevent state updates on unmounted components.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/openhuman/cron/ops.rs`:
- Around line 277-290: The current flow deletes the queued placeholder with
cron::delete_queued_runs and then separately calls cron::record_run and
cron::record_last_run, which can leave no history if the later writes fail;
change this to a single atomic operation by using a DB transaction or an
upsert/update-in-place: introduce or call a transactional helper (e.g., a new
cron::finalize_run_transactional or cron::upsert_run) that in one DB transaction
replaces the queued placeholder with the terminal row and updates the last-run
metadata, or modify cron::record_run to detect and update an existing queued row
instead of deleting then inserting; ensure cron::delete_queued_runs,
cron::record_run, and cron::record_last_run are executed inside the same
transaction and propagate errors instead of ignoring them.
---
Outside diff comments:
In `@app/src/pages/Routines.tsx`:
- Around line 127-138: The handleRunNow flow currently updates state after
awaiting openhumanCronList() and in catch/finally without checking component
mount status; wrap all post-await state setters (setJobs(...),
setError(message)) and the cleanup call removeBusy(key) in an
isMountedRef.current guard similar to the polling loop so they only run if
isMountedRef.current is true; locate handleRunNow and add conditional checks
around the setJobs call after sorting the response, around setError in the catch
block, and around removeBusy(key) in the finally block to prevent state updates
on unmounted components.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 2d3e2348-5289-4260-9c7c-0a8cc3adaf62
📒 Files selected for processing (21)
app/src/components/intelligence/memoryGraphLayout.test.tsapp/src/lib/i18n/ar.tsapp/src/lib/i18n/bn.tsapp/src/lib/i18n/de.tsapp/src/lib/i18n/en.tsapp/src/lib/i18n/es.tsapp/src/lib/i18n/fr.tsapp/src/lib/i18n/hi.tsapp/src/lib/i18n/id.tsapp/src/lib/i18n/it.tsapp/src/lib/i18n/ko.tsapp/src/lib/i18n/pl.tsapp/src/lib/i18n/pt.tsapp/src/lib/i18n/ru.tsapp/src/lib/i18n/zh-CN.tsapp/src/pages/Routines.tsxapp/src/utils/tauriCommands/cron.tssrc/openhuman/cron/mod.rssrc/openhuman/cron/ops.rssrc/openhuman/cron/ops_tests.rssrc/openhuman/cron/store.rs
✅ Files skipped from review due to trivial changes (12)
- src/openhuman/cron/mod.rs
- app/src/lib/i18n/id.ts
- app/src/lib/i18n/ar.ts
- app/src/lib/i18n/it.ts
- app/src/lib/i18n/es.ts
- app/src/lib/i18n/fr.ts
- app/src/lib/i18n/de.ts
- app/src/lib/i18n/zh-CN.ts
- app/src/lib/i18n/hi.ts
- app/src/lib/i18n/bn.ts
- app/src/lib/i18n/pl.ts
- app/src/lib/i18n/en.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- app/src/utils/tauriCommands/cron.ts
- src/openhuman/cron/ops_tests.rs
| // Remove the "queued" placeholder before inserting the real result | ||
| // so we don't leave orphaned rows in the run history. | ||
| let _ = cron::delete_queued_runs(&config_owned, &job.id); | ||
|
|
||
| let _ = cron::record_run( | ||
| &config_owned, | ||
| &job.id, | ||
| started_at, | ||
| finished_at, | ||
| status, | ||
| Some(&output), | ||
| duration_ms, | ||
| ); | ||
| let _ = cron::record_last_run(&config_owned, &job.id, finished_at, success, &output); |
There was a problem hiding this comment.
Finalize queued runs atomically.
Line 279 deletes the placeholder before Line 281 persists the terminal row, and both writes ignore failures. If the insert/update path fails after the delete, this run vanishes from history entirely and the frontend can sit in "queued" until timeout even though execution already finished. Please collapse this into one transactional store operation, or update the placeholder row in place instead of delete-then-insert.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/openhuman/cron/ops.rs` around lines 277 - 290, The current flow deletes
the queued placeholder with cron::delete_queued_runs and then separately calls
cron::record_run and cron::record_last_run, which can leave no history if the
later writes fail; change this to a single atomic operation by using a DB
transaction or an upsert/update-in-place: introduce or call a transactional
helper (e.g., a new cron::finalize_run_transactional or cron::upsert_run) that
in one DB transaction replaces the queued placeholder with the terminal row and
updates the last-run metadata, or modify cron::record_run to detect and update
an existing queued row instead of deleting then inserting; ensure
cron::delete_queued_runs, cron::record_run, and cron::record_last_run are
executed inside the same transaction and propagate errors instead of ignoring
them.
There was a problem hiding this comment.
I see there are pending changes requested on this PR. Since CI is currently failing and there are active review comments, I'll hold off on a fresh review until those are addressed. CI and code review feedback must both be resolved before approval.
Wrap setJobs, setError, and removeBusy calls outside the polling loop with isMountedRef.current checks to prevent React state updates on unmounted components when the user navigates away mid-request. Addresses @coderabbitai on Routines.tsx:127-138
| Some(&output), | ||
| duration_ms, | ||
| ); | ||
| let _ = cron::record_last_run(&config_owned, &job.id, finished_at, success, &output); |
There was a problem hiding this comment.
Deferred — atomicity of delete_queued_runs + record_run + record_last_run (CodeRabbit suggestion)
The current sequential flow (delete placeholder → insert terminal row → update last_run) is not wrapped in a single DB transaction. However, each of these calls independently goes through with_connection which opens a new SQLite connection, so they can't share a transaction without refactoring the store layer.
In practice the risk is minimal: SQLite is single-writer, the window between operations is microseconds, and partial failure only means one manual "Run Now" click doesn't record its result — no data corruption or user-visible misbehavior. The fix would require threading a transaction through the store API, which is out of scope for this PR.
Recommend addressing in a follow-up if the store layer gets a transaction-aware API.
sanil-23
left a comment
There was a problem hiding this comment.
I see @graycyrus has requested changes — deferring to their feedback. Will review once those are addressed.
Summary
cron_runRPC handler to be non-blocking: spawns job execution in a backgroundtokio::taskand returns{status: "queued"}immediatelyACTIVE_RUNSguard (Lazy<Mutex<HashSet<String>>>) to reject duplicate concurrent invocations of the same jobopenhumanCronRunTypeScript return type to includestatus: 'queued' | 'ok' | 'error'with optionalduration_msandoutputhandleRunNowinRoutines.tsxto pollopenhumanCronRunsevery 2 s (max 120 s) whenstatus === 'queued', keeping the "Running…" spinner active throughoutProblem
cron_runhandler awaitedexecute_job_now().awaitinline, so any long-running agent job would hold the RPC thread — the frontend would time out after ~30 s and show an error even if the job was still runningSolution
tokio::spawn: the RPC handler inserts the job ID intoACTIVE_RUNS, spawns the task, and returnsqueuedin < 1 msexecute_job_now, records the run, updateslast_run/last_status, callsdeliver_job, then removes the ID fromACTIVE_RUNSSubmission Checklist
Impact
Related
AI Authored PR Metadata
Linear Issue
Commit & Branch
Validation Run
cargo test -p openhuman cronBehavior Changes
Parity Contract
timeoutso history remains accurateDuplicate / Superseded PR Handling
Summary by CodeRabbit