Skip to content

Scheduled tasks are aborted by browser heartbeats — stop_background_tasks_for_foreground() fires unconditionally, ignoring BACKGROUND_TASK_FOREGROUND_GATE #5536

Description

@Novellamaia

Prerequisites

  • I searched open issues and discussions and did not find an existing report of this bug.
  • This is not a security vulnerability. (Vulnerabilities go to GitHub Security Advisories — see SECURITY.md.)
  • I am running the latest code from the dev branch (the default branch you get on clone, where fixes land first) and the bug still reproduces there. Please git pull the latest dev before filing.

Install Method

Docker (docker compose up)

Operating System

Linux

Steps to Reproduce

  1. Docker Compose deployment, dev branch (2026-07-14), Ubuntu Server 26.04.
  2. Create a scheduled Prompt task (daily, any model — reproduced with qwen2.5:7b/14b and qwen3-coder:480b-cloud).
  3. Leave any Odysseus client open — a browser tab, or the PWA installed on a phone (which keeps heartbeating in the background).
  4. Let the task trigger (scheduled or via Run again).
  5. Observe the run in Tasks → Activity and in the task_runs table.

Expected Behaviour

The scheduled run executes to completion. If a foreground gate is desired, BACKGROUND_TASK_FOREGROUND_GATE=false should disable it entirely, and in any case a background run should be delayed/paused rather than aborted by ordinary UI polling.

Actual Behaviour

Every scheduled run is aborted within seconds of starting. task_runs rows show:

status=aborted, error="Stopped by user", result="Starting…" (or "Queued — waiting for Odysseus to be idle…")
started 2026-07-14 04:30:00, finished 2026-07-14 04:30:01 (1.5s)

The user did not stop anything — the run is killed by the app's own heartbeat/polling.

Cause (dev, 2026-07-14):

  • app.py:628 — the /api/activity/heartbeat route calls task_scheduler.stop_background_tasks_for_foreground(reason="browser heartbeat") unconditionally. There is no check of BACKGROUND_TASK_FOREGROUND_GATE (or any other flag) on this path.
  • app.py:199_InteractiveActivityMiddleware also calls it for tracked foreground requests, gated only by should_track_interactive_request(path, method).
  • src/interactive_gate.py:24BACKGROUND_TASK_FOREGROUND_GATE exists and is honoured by the waiting logic (wait_for_interactive_quiet), but nothing on the cancellation path consults it.
  • src/task_scheduler.py:947-951 — the run dies via asyncio.CancelledError; because foreground_cancel["hit"] is not set on this path, it is recorded with the misleading message "Stopped by user" rather than "Paused because Odysseus became active".

Net effect: with any client open (a phone PWA is enough, even in the background), scheduled tasks can never complete, and BACKGROUND_TASK_FOREGROUND_GATE=false does not help. This is easy to miss because the failure is silent and the error message blames the user.

Setting BACKGROUND_TASK_MAX_WAIT_SECONDS=120 and BACKGROUND_TASK_FOREGROUND_GATE=false (both verified live in the container via printenv) does not prevent the aborts.

Workaround: patching out the asyncio.create_task(_stop_background()) call in the /api/activity/heartbeat route makes scheduled tasks run to completion, even with the UI open. After that patch, an identical task completed successfully in ~30s and delivered its notification.

Note: BACKGROUND_TASK_* variables also have to be added to the environment: block of the odysseus service in docker-compose.yml before they reach the container at all — they are read by interactive_gate.py but not declared in compose. That is arguably a second (smaller) issue: setting them in .env alone silently does nothing.

Logs / Screenshots

2026-07-13 20:xx - src.task_scheduler - INFO - Stopped 2 background scheduler task(s): browser heartbeat
2026-07-13 20:xx - src.task_scheduler - INFO - Stopped 4 background scheduler task(s): foreground request GET /api/email/unread-state
2026-07-13 20:xx - src.task_scheduler - INFO - Task 'Briefing TEST' Stopped by user

task_runs rows (all aborted seconds after start, error="Stopped by user"):
('3db33ace...', 'aborted', '2026-07-14 17:17:21', '2026-07-14 17:17:34', 'Starting…', 'Stopped by user')
('e4b7ffb5...', 'aborted', '2026-07-14 04:30:00', '2026-07-14 04:30:01', 'Starting…', 'Stopped by user')

Model / Backend (if relevant)

Ollama (Docker) + qwen2.5:7b / qwen2.5:14b / qwen3-coder:480b-cloud — model-independent, scheduler-side

Are you willing to submit a fix?

Partially — I can help but need guidance

Additional Information

Happy to test a fix branch — Docker + Ollama + HA MCP setup available for repro.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingready for reviewDescription complete — ready for maintainer review

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions