diff --git a/src/task_scheduler.py b/src/task_scheduler.py index c94c19de3..df515ebea 100644 --- a/src/task_scheduler.py +++ b/src/task_scheduler.py @@ -687,6 +687,12 @@ class TaskScheduler: db = SessionLocal() try: now = _utcnow() + foreground_active = False + try: + from src.interactive_gate import has_foreground_activity + foreground_active = has_foreground_activity() + except Exception: + foreground_active = False async with self._executing_lock: # Snapshot under the lock so we don't race with mid-iteration adds. executing_snapshot = set(self._executing) @@ -700,8 +706,13 @@ class TaskScheduler: for task in due: if task.id in self._executing: continue + if foreground_active: + task.next_run = now + timedelta(minutes=15) + continue self._executing.add(task.id) to_dispatch.append(task.id) + if foreground_active and due: + db.commit() for task_id in to_dispatch: asyncio.create_task(self._execute_task(task_id)) finally: @@ -754,6 +765,7 @@ class TaskScheduler: # If cancellation happens while queued behind the semaphore, # _execute_task_locked never runs and cannot update the Activity row. self._mark_run_aborted(task_id, run_id) + self._defer_immediately_due_task(task_id, delay=timedelta(minutes=15)) raise finally: handle = self._task_handles.get(task_id) @@ -763,6 +775,28 @@ class TaskScheduler: async with self._executing_lock: self._executing.discard(task_id) + def _defer_immediately_due_task(self, task_id: str, *, delay: timedelta): + """A queued task can be cancelled before _execute_task_locked gets a DB + handle. If its next_run stays in the past, the scheduler dispatches it + again on the next tick and spams aborted Activity rows.""" + try: + from core.database import SessionLocal, ScheduledTask + db = SessionLocal() + try: + task = db.query(ScheduledTask).filter(ScheduledTask.id == task_id).first() + if ( + task + and task.status == "active" + and task.next_run is not None + and task.next_run <= _utcnow() + ): + task.next_run = _utcnow() + delay + db.commit() + finally: + db.close() + except Exception: + logger.debug("Failed to defer cancelled queued task %s", task_id, exc_info=True) + async def _execute_task_locked( self, task_id: str, diff --git a/static/app.js b/static/app.js index be7d3f863..5888f517c 100644 --- a/static/app.js +++ b/static/app.js @@ -22,7 +22,7 @@ import memoryModule from './js/memory.js'; import voiceRecorderModule from './js/voiceRecorder.js'; import censorModule from './js/censor.js'; import galleryModule from './js/gallery.js'; -import tasksModule from './js/tasks.js'; +import tasksModule from './js/tasks.js?v=20260630tasksactivity'; import calendarModule from './js/calendar.js'; import notesModule from './js/notes.js'; import adminModule from './js/admin.js'; diff --git a/static/index.html b/static/index.html index 4515a4fad..d19fffa5f 100644 --- a/static/index.html +++ b/static/index.html @@ -219,8 +219,8 @@ }, { once: true }); })(); - - + + @@ -2512,7 +2512,7 @@ - + diff --git a/static/js/tasks.js b/static/js/tasks.js index bde11d9e2..4172a429b 100644 --- a/static/js/tasks.js +++ b/static/js/tasks.js @@ -1948,7 +1948,7 @@ async function _renderActivityView() { const body = modal?.querySelector('.modal-body'); if (!body) return; body.innerHTML = ` -