Skip to content

Release Notes

v0.9.0 - Management & Observability API (Phase 5/6) - 2026-09-05

Phase 5 of the roadmap to v1.0.0 adds three management features: update_task(), rich job/task queries, and stats(). See the new Observability docs page.

What's new

  • update_task() — changes a scheduled task in place and keeps its task_id. You can change the name, interval, scheduling mode, args/kwargs, timeout, retry settings, jitter, and the progress callback. All parameters are keyword-only; omit the parameters you do not want to change. timeout=None disables the timeout, and progress_callback=None removes the callback. When you change interval, quiv reschedules the next run to now + interval. When you update a running task, the changes apply from its next run. The method emits the new Event.TASK_UPDATED with the updated Task. You cannot change run_once, delay, or the handler func.
  • Rich job/task queriesget_all_jobs() accepts new filters: task_id, and since/until for a time window on started_at (pass timezone-aware UTC values). It also accepts order_by ("started_at" or "ended_at"), descending, and limit/offset for pagination. get_all_tasks() accepts status, limit, and offset, and returns tasks ordered by next_run_at.
  • stats() — returns a QuivStats snapshot (a frozen dataclass, exported from quiv). It contains the active job count, the pool size and utilization, task counts by status, the earliest upcoming run, and the retained job-history count.
  • The FastAPI example app has three new endpoints: GET /tasks/stats, GET /tasks/{task_id}/jobs?limit=&offset=, and PATCH /tasks/{task_id}.

Fixes

  • add_task() no longer requires an interval for a run-once task (#65). A run-once task never repeats, so quiv never reads its interval. Omit interval when you pass run_once=True. An interval given with run_once=True is ignored, and Task.interval_seconds reads None. A recurring task still requires interval > 0.
  • interval=None now raises ConfigurationError (#65). Earlier versions raised TypeError from an unguarded comparison. None now reaches the same error as 0 and -1.
  • run_task_immediately() raises TaskNotFoundError for an unknown task id (#67). Earlier versions raised HandlerNotRegisteredError, which names a different fault. get_task(), remove_task() and run_task_immediately() now report the same error for the same cause. A run-once task deletes itself when it finishes, so its id stops resolving after it runs. HandlerNotRegisteredError keeps its own meaning: the task exists, but no handler is registered for it.

Behavior changes

  • TaskNotScheduledError is deprecated. quiv no longer raises it. It is now a subclass of TaskNotFoundError, and the name stays exported, so imports keep working. An except TaskNotScheduledError clause no longer catches these errors. Change it to except TaskNotFoundError. quiv removes the alias in 1.0.0.

Full Changelog: https://github.com/nandyalu/quiv/compare/v0.8.0...v0.9.0

Changes

v0.8.0 - Execution Features (Phase 4/6) - 2026-08-21

Phase 4 of the roadmap to v1.0.0 adds three failure-handling features: per-task timeout, retry with exponential backoff, and jitter. See the new Failure Handling docs page.

What's new

  • Per-task timeoutadd_task(..., timeout=30) sets a time limit for each job. When a job runs longer than timeout seconds, quiv sets the job's stop event, in the same way as cancel_job(). The job then finalizes as cancelled with a timeout error message. The timeout is cooperative. If the handler ignores its stop event, it keeps its pool thread until it returns; quiv never kills threads. The scheduler loop wakes for the nearest timeout deadline, so a timeout fires within milliseconds of that deadline.
  • Retry with exponential backoffadd_task(..., max_retries=3, retry_backoff=10) runs a failed job again. A job is failed when an exception escapes the handler. The next attempt starts after retry_backoff * 2**(failures - 1) seconds: the first retry waits retry_backoff seconds, the second waits twice that, and so on. Cancelled jobs do not retry; this includes timeouts. A successful run resets the failure counter. When retries are exhausted, a recurring task returns to its normal schedule and a run-once task is deleted. Each Job records its attempt number. The new Event.JOB_RETRYING fires after JOB_FAILED when quiv schedules a retry.
  • Jitteradd_task(..., jitter=5) adds a random offset between 0 and jitter seconds to each next run of a recurring task. Use it when many tasks share the same interval boundaries and would start at the same time. quiv draws a new offset for every run. Jitter does not apply to the initial delay or to retry backoff.
  • Task exposes the new fields timeout_seconds, max_retries, retry_backoff_seconds, retry_attempt, and jitter_seconds. Job exposes attempt.
  • The four new options are keyword-only. The rest of the add_task() signature is unchanged, including positional args, kwargs, and progress_callback. Existing calls continue to work.

Fixes

  • Fixed-interval scheduling could set next_run_at to a time that is not in the future. This happened when a job finished within clock resolution of its start time, or when the elapsed time landed exactly on an interval boundary. The task then dispatched again immediately. The next run is now always the next interval boundary that is strictly in the future.
  • When a timed-out job's handler also raised an exception, the job's error_message showed only the exception text and hid the timeout. The timeout message now comes first, and the handler's exception is appended.

Full Changelog: https://github.com/nandyalu/quiv/compare/v0.7.0...v0.8.0

Changes

v0.7.0 - Database Locking Rework (Phase 3/6) - 2026-08-09

Phase 3 of the roadmap to v1.0.0: lock-free reads under SQLite WAL.

What's new

  • Lock-free reads — read queries (get_task, get_job, get_all_tasks, get_all_jobs, due-task queries) no longer serialize behind the persistence layer's global lock; SQLite WAL mode provides the reader/writer coordination. Read-modify-write operations (task/job lifecycle transitions, pause/resume, cleanup) still serialize on a dedicated write lock, preserving the no-lost-update guarantees.
  • SQLite pragmas — connections now set synchronous=NORMAL (the standard WAL pairing: fsync on checkpoint instead of per-commit; the DB is an ephemeral temp file, so durability-on-crash was never a goal) and an explicit busy_timeout=10000 matching the existing driver-level timeout.

Performance

With a writer and a heavy get_all_jobs reader running concurrently, p99 latency of a small get_task read drops from ~89 ms to ~1.1 ms (~80×) — small reads no longer queue behind large scans or writes on a global lock.

Aggregate wall time on a synthetic hammer benchmark (5,000 mixed ops, 70% reads / 30% writes, 16 threads) measured 31.0 s before vs 44.5 s after. Investigated per the phase plan's guard: not connection-pool contention (unchanged with a 32-connection pool) and not WAL checkpoint starvation (a 20 MiB WAL with serialized reads stayed fast) — it is CPython GIL convoying on CPU-bound ORM deserialization when all 16 threads busy-loop, where serialized execution is faster in aggregate. An artifact of the synthetic saturation workload, not of realistic loads, and one that disappears on free-threaded builds.

The synchronous=NORMAL pragma alone improves write throughput ~20% (31.0 s → 24.6 s with reads still serialized).

Housekeeping

  • New concurrency stress-test suite (tests/test_persistence_concurrency.py): concurrent writers with no lost updates, readers seeing consistent rows during writes, a full scheduler run at pool_size=32 under constant read load, and a pause/resume race.
  • Docs, release notes, and CLAUDE.md updated; version bumped to 0.7.0.

Full Changelog: https://github.com/nandyalu/quiv/compare/v0.6.0...v0.7.0

Changes

v0.6.0 - Scheduler Core Efficiency (Phase 2/6) - 2026-08-05

Phase 2 of the roadmap to v1.0.0: smart sleep loop and handler signature caching.

What's new

  • Sub-second intervals — the scheduler loop now sleeps until the next due task on an interruptible wait instead of polling every second. add_task(interval=0.2) works; dispatch jitter drops from up to ~1 s to milliseconds.
  • Zero idle polling — an idle scheduler issues no database queries (bounded by a 60-second safety-net wake-up). Schedule changes (add_task, run_task_immediately, resume_task, remove_task) and job completions wake the loop immediately, so deferred tasks dispatch as soon as a pool slot frees and shutdown() returns promptly instead of waiting out the current sleep.
  • Handler signature caching — each handler's injectable kwargs (_job_id, _stop_event, _progress_hook) are introspected once per handler lifetime (weakly cached) instead of three inspect.signature() calls per dispatch.

Fixes

  • When the pool is saturated, the loop no longer busy-polls the database at ~100 Hz over overdue tasks it cannot dispatch anyway; it sleeps until a finishing job wakes it.

Housekeeping

  • 7 new tests: timing tests for dispatch latency, sub-second runs, idle no-polling, backpressure dispatch-on-slot-free, and prompt shutdown, plus a smoke test codifying the manual 0.1 s-interval throughput check. Docs, release notes, and AI-tooling artifacts updated; version bumped to 0.6.0.

Full Changelog: https://github.com/nandyalu/quiv/compare/v0.5.0...v0.6.0

Changes

v0.5.0 - Correctness & Stability (Phase 1/6) - 2026-07-26

Phase 1 of the roadmap to v1.0.0: three concurrency bug fixes.

Behavior changes

  • run_task_immediately() now raises TaskNotActiveError when the task is not active. Previously a running task could be dispatched a second time concurrently (breaking the no-overlap guarantee) and a paused task was silently un-paused. Resume paused tasks explicitly with resume_task().

What's new

  • shutdown(timeout=...) / stop(timeout=...) — optional bound on how long shutdown waits for the scheduler thread and in-flight jobs. Jobs that do not exit within the deadline are abandoned on their worker threads with a warning, so a hung handler can no longer block application shutdown forever. Default (timeout=None) keeps the previous wait-forever behavior.
  • New exception TaskNotActiveError (exported from quiv).

Fixes

  • remove_task() racing the dispatch loop could raise a KeyError that stalled the scheduler for 5 seconds; dispatch now skips removed tasks gracefully, and the shared handler/callback/stop-event registries are protected by a lock.
  • shutdown() now signals cancellation only to running jobs instead of scanning the entire retained job history.

Housekeeping

  • Roadmap phases renumbered: v0.3.x/v0.4.x were already consumed by earlier feature releases, so Phase 1 ships as v0.5.0 and later phases shift down accordingly (Phase 6 stays v1.0.0).
  • 7 new regression tests; docs, release notes, and AI-tooling artifacts updated; version bumped to 0.5.0.
  • Docs cleanup: all Markdown files unwrapped to one paragraph per continuous line (zensical can break rendering when a sentence/paragraph is split across source lines). No content changes.

Changes

v0.4.1 - AI Tooling & Async teardown fix - 2026-07-20

What's new

  • AI tooling — quiv now teaches AI coding assistants (Claude Code, Cursor, Copilot, …) how to use it correctly, in #51:

    • Packaged agent guide — every install ships a condensed agent-facing reference at quiv/AGENTS.md inside the package (lands in site-packages next to the code): API surface, handler injection rules, cancellation semantics, FastAPI wiring, and common pitfalls. AI tools exploring installed dependencies pick it up with zero setup.
    • llms.txt — the docs site publishes llms.txt (index linking every docs page as raw Markdown) and llms-full.txt (the full docs in one file) following the llms.txt convention.
    • Claude Code plugin — the quiv repository doubles as a plugin marketplace shipping a quiv skill that Claude loads automatically whenever a conversation touches quiv. Install once with /plugin marketplace add nandyalu/quiv then /plugin install quiv@quiv; /plugin update quiv picks up the latest guidance.

    See the new AI Tools docs page for all three entry points.

Fixes

  • Per-invocation async event loops now finalize outstanding async generators and shut down the loop's default executor before closing. Previously, if a handler raised while an async generator was still active, Python could emit Task was destroyed but it is pending and async_generator_athrow was never awaited warnings during teardown. The original handler exception remains visible. by @d4rk22 in #50

Other changes

  • ci: skip coverage PR comment for fork PRs
  • upd: roadmap for v1.0.0
  • build(deps-dev): bump zensical from 0.0.43 to 0.0.45 by @dependabot[bot] in #42
  • build(deps): bump actions/checkout from 6 to 7 by @dependabot[bot] in #44
  • build(deps-dev): bump pytest from 9.0.3 to 9.1.1 by @dependabot[bot] in #45
  • build(deps): bump sqlmodel from 0.0.38 to 0.0.39 by @dependabot[bot] in #46
  • build(deps-dev): bump zensical from 0.0.45 to 0.0.50 by @dependabot[bot] in #47
  • build(deps): bump tzdata from 2026.2 to 2026.3 by @dependabot[bot] in #48
  • build(deps-dev): bump mypy from 2.1.0 to 2.2.0 by @dependabot[bot] in #49
  • fix: finalize async resources before closing job loops by @d4rk22 in #50

New Contributors

Full Changelog: https://github.com/nandyalu/quiv/compare/v0.4.0...v0.4.1

Changes

v0.4.0 - run_on_main helper method - 2026-05-31

What's new

  • quiv.run_on_main(func, *args, **kwargs) — fire-and-forget helper that dispatches a callable onto the active Quiv instance's main event loop. Importable at module level (from quiv import run_on_main) and callable from anywhere in a task handler's call stack - no _progress_hook parameter to thread through intermediate functions.

    • Auto-detects whether the caller is already on the main loop's thread: sync targets run inline on-loop and async targets are scheduled via main_loop.create_task. From a worker thread, sync targets dispatch via call_soon_threadsafe and async targets via run_coroutine_threadsafe.
    • The same helper works from a FastAPI route handler on the main loop and from inside a Quiv task — one utility shared between request and task code (e.g., a WebSocket broadcast whose connected-clients state lives on uvicorn's loop).
    • Exceptions raised by the target are logged on the active Quiv's logger and swallowed, mirroring _progress_hook and event-listener semantics.

    See Running on the main event loop for the full walkthrough.

Other changes

  • Quiv.start() now registers the instance as the process-level "active" Quiv (cleared by shutdown()) so run_on_main can resolve a target loop when called outside a task context. Multiple concurrent instances log a warning naming the most recently started instance as the winner for out-of-task callers.
  • Inside _run_job, a ContextVar is set to the running Quiv instance for the duration of each handler invocation; this propagates into nested sync calls, the per-job async event loop, and asyncio.create_task spawned inside an async handler.

Full Changelog: https://github.com/nandyalu/quiv/compare/v0.3.5...v0.4.0

Changes

v0.3.5 - Cleanup noise & update packages - 2026-05-27

What's new

  • Removed a debug logging statement from the set_timezone_to_utc method to reduce log verbosity by @nandyalu in #38

Other changes

  • Updated various python libraries to latest versions.
  • build(deps): bump actions/upload-pages-artifact from 4 to 5 by @dependabot[bot] in #24
  • build(deps-dev): bump zensical from 0.0.32 to 0.0.33 by @dependabot[bot] in #25
  • build(deps-dev): bump mypy from 1.20.0 to 1.20.1 by @dependabot[bot] in #26
  • build(deps-dev): bump zensical from 0.0.33 to 0.0.36 by @dependabot[bot] in #29
  • build(deps): bump tzdata from 2026.1 to 2026.2 by @dependabot[bot] in #28
  • build(deps-dev): bump mypy from 1.20.1 to 1.20.2 by @dependabot[bot] in #27
  • build(deps): bump orgoro/coverage from 3.2 to 3.3 by @dependabot[bot] in #30
  • build(deps-dev): bump zensical from 0.0.36 to 0.0.39 by @dependabot[bot] in #31
  • build(deps-dev): bump zensical from 0.0.39 to 0.0.41 by @dependabot[bot] in #32
  • build(deps-dev): bump mypy from 1.20.2 to 2.0.0 by @dependabot[bot] in #33
  • build(deps-dev): bump zensical from 0.0.41 to 0.0.42 by @dependabot[bot] in #34
  • build(deps-dev): bump mypy from 2.0.0 to 2.1.0 by @dependabot[bot] in #35
  • build(deps): bump pymdown-extensions from 10.21.2 to 10.21.3 in the uv group across 1 directory by @dependabot[bot] in #36
  • build(deps-dev): bump zensical from 0.0.42 to 0.0.43 by @dependabot[bot] in #37

Full Changelog: https://github.com/nandyalu/quiv/compare/v0.3.4...v0.3.5

Changes

v0.3.4 - Job and Task attrs datetime aware - 2026-04-10

Bug fixes

  • Job datetime normalization: Job datetimes (started_at, ended_at) loaded from SQLite are now correctly normalized to UTC-aware. Previously, the @model_validator on Job was dead code because SQLAlchemy bypasses Pydantic validators when hydrating table classes.

Improvements

  • Generic datetime normalization via @reconstructor: Added a @reconstructor method on QuivModelBase that automatically normalizes all datetime fields to UTC-aware on every DB load. This applies to both TaskDB and Job models (and any future models), replacing the per-model dead-code validators.
  • Removed dead @model_validator from TaskDB and Job models.
  • delete_task() now has test coverage for TaskNotFoundError on missing task.
  • updated python packages to latest versions.

Documentation

  • New Testing page documenting all 108 tests with a breakdown of edge cases covered across 18 categories.

CI

  • docs-release.yml now waits for the release to be visible in the GitHub API before generating the changelog, fixing a race condition where the current release was missing from the generated docs.

Changes

v0.3.3 - fixed inteval option for tasks - 2026-04-09

Breaking changes

  • Default interval scheduling changed to fixed intervals: fixed_interval defaults to True, meaning next run is now scheduled from the job start time rather than completion time. Set fixed_interval=False to restore the previous wait-between-runs behavior.

What's new

  • fixed_interval per-task scheduling mode: add_task() accepts a new fixed_interval parameter:

    • True (default) — next run at fixed intervals from job start time. If a run exceeds the interval, missed intervals are skipped.
    • False — next run interval seconds after job completion (old behavior).

Other changes

  • finalize_task_after_job() accepts job_started_at for fixed-interval scheduling.

Full Changelog: https://github.com/nandyalu/quiv/compare/v0.3.2...v0.3.3

Changes

v0.3.2 - Removed unique task name constraint - 2026-04-09

Breaking changes

  • Task operations now use task_id instead of task_name: All public methods that previously accepted a task_name string now accept the task_id (UUID string) returned by add_task(). This removes the uniqueness constraint on task names — multiple tasks can now share the same task_name.

    Affected methods: - remove_task(task_id) — previously remove_task(task_name) - pause_task(task_id) — previously pause_task(task_name) - resume_task(task_id) — previously resume_task(task_name) - run_task_immediately(task_id) — previously run_task_immediately(task_name) - get_task(task_id) — previously get_task(task_name) (by-name lookup)

    Removed methods: - get_task_by_id() — merged into get_task(task_id)

    Migration: Store the return value of add_task() and pass it to all task operations:

    # Before
    scheduler.add_task("my-task", handler, interval=60)
    scheduler.pause_task("my-task")
    
    # After
    task_id = scheduler.add_task("my-task", handler, interval=60)
    scheduler.pause_task(task_id)
    
  • Event listeners receive typed model objects instead of dicts

    Event listener callbacks now receive Task and Job model objects directly instead of untyped dict[str, Any].

    • TASK_* events: callback(event: Event, task: Task)
    • JOB_* events: callback(event: Event, task: Task, job: Job)

    Migration:

    # Before
    def on_completed(event, data):
        print(data["task_name"], data["duration"])
    
    # After
    from quiv.models import Task, Job
    
    def on_completed(event: Event, task: Task, job: Job):
        print(task.task_name, job.duration_seconds)
    

What's new

  • Duplicate task names allowed

    add_task() no longer raises ConfigurationError on duplicate task_name. Each call returns a unique task_id (UUID), so multiple tasks can share a display name. This is especially useful for one-shot tasks that may be scheduled repeatedly with the same name.

  • duration_seconds and error_message on the Job model

    The Job model now includes two new fields:

    • duration_seconds: float | None — job runtime in seconds, set when the job finishes
    • error_message: str | None — error description, set when a job fails

    Both fields are persisted in the database and available via get_job() and get_all_jobs(), making it easy to inspect job history without parsing logs.

  • Typed event listener callbacks

    Event listeners now receive real Task and Job model objects with full IDE autocomplete and type checking. JOB_* events include the parent Task alongside the Job, so listeners have full context without extra lookups.

Documentation

  • Updated all docs to reflect task_id-based API across getting-started, API reference, architecture, event listeners, bigger applications, progress callbacks, and exceptions pages.
  • Updated all code examples to use task_id for runtime operations.
  • Added admonitions and footnotes throughout docs for better readability.
  • Added _job_id tracing section to the "Why quiv?" page, describing how Trailarr uses injected job IDs as trace context for log correlation.
  • Rewrote event listeners documentation with typed callback signatures, updated examples, and new FastAPI WebSocket example using model objects.

Other changes

  • Internal handler and progress callback registries are now keyed by task_id instead of task_name.
  • prepare_invocation() in the execution layer uses task_id for progress callback dispatch.
  • Removed get_task_by_name(), get_task_id_by_name() from the persistence layer.
  • Renamed get_task_by_id() to get_task() in the persistence layer.
  • delete_task() and queue_task_for_immediate_run() in the persistence layer now accept task_id instead of task_name.
  • finalize_job() now accepts optional duration_seconds and error_message parameters.
  • Removed unused timezone import from persistence module.

Full Changelog: https://github.com/nandyalu/quiv/compare/v0.3.1...v0.3.2

Changes

v0.3.1 - Better task pickling - 2026-04-07

Breaking Changes

  • The public methods get_task(), get_task_by_id(), and get_all_tasks() return Task objects with unpickled args (tuple) and kwargs (dict) — ready for JSON serialization in FastAPI endpoints.
  • Internal model renamed: The SQLModel database model is now TaskDB (internal only, not exported). External model Task is still the same - but is now only used for Public API responses and has correct types.

What's New

  • Async callbacks without event loop: Async progress callbacks and event listeners now run in a temporary event loop when no main loop is available, instead of being skipped with a warning.
  • Eager main loop resolution: The main event loop is now resolved at start() time (in addition to lazy resolution on first callback), improving reliability in FastAPI apps.

Documentation

  • Added return types to all method signatures in API docs (e.g., get_task(task_name: str) -> Task.
  • Updated architecture, progress-callbacks, and event-listeners docs to reflect new async callback behavior

Changes by @nandyalu in #20

Full Changelog: https://github.com/nandyalu/quiv/compare/v0.3.0...v0.3.1

Changes

v0.3.0 - Better pickling and Event listeners - 2026-04-07

What's Changed

  • Changed argument serialization for task persistence from JSON to pickle, allowing most Python objects (except lambdas and inner functions) to be scheduled as arguments by @nandyalu in #19
  • Added support for global event listeners via add_listener(event, callback) and remove_listener(event, callback), including both sync and async callbacks, with robust dispatch and error handling. Events include all major task and job lifecycle transitions. by @nandyalu in #19
  • Introduced startup() as an alias for start(), and stop() as an alias for shutdown(), making start/stop pairs more natural in user code by @nandyalu in #19
  • Job.id now uses UUID and can be injected into task function (as _job_id) if function accepts it - can be used for task tracing / logging by @nandyalu in #19
  • Updated all relevant documentation by @nandyalu in #19

Full Changelog: https://github.com/nandyalu/quiv/compare/v0.2.4...v0.3.0

Changes

v0.2.4 - Preserve task args order - 2026-04-07

What's Changed

  • add_task args as tuple to preserve order by @nandyalu in #18
  • logger accepts logging.Logger as well as logging.LoggerAdapter by @nandyalu in #18

Full Changelog: https://github.com/nandyalu/quiv/compare/v0.2.3...v0.2.4

Changes

v0.2.3 - Exception logging improvements - 2026-04-06

What's Changed

Full Changelog: https://github.com/nandyalu/quiv/compare/v0.2.2...v0.2.3

Changes

v0.2.2 - SQLModel Registry Fix - 2026-03-13

What's Changed

  • fix: quiv registry to not include user models. Updated the private registry usage for SQLModel models of Quiv to the method from https://github.com/fastapi/sqlmodel/discussions/1539#discussioncomment-14229572 by @nandyalu in #7

  • Added a test to ensure user's SQLModel with table=True does not get created in Quiv database by @nandyalu in #7

Full Changelog: https://github.com/nandyalu/quiv/compare/v0.2.0...v0.2.2

Changes

v0.2.1 - Fix Release Build - 2026-03-11

What's Changed

  • Fix release build and auto update release-notes in docs by @nandyalu in #6

Full Changelog: https://github.com/nandyalu/quiv/compare/v0.2.0...v0.2.1

Changes

v0.2.0-Bug Fixes and minor updates - 2026-03-09

Breaking Changes

  • timezone_name renamed to timezone — Both Quiv() and QuivConfig() now use timezone for the display timezone parameter. Update any timezone_name= keyword arguments.
  • register_handler / register_progress_callback are now private — Renamed to _register_handler / _register_progress_callback. Use add_task() instead, which handles registration internally.
  • add_task() raises on duplicate task names — Call remove_task() first if you need to replace a task.

New Features

  • remove_task(task_name) — Remove a task and its handler/callback registrations.
  • Cooperative cancellation_stop_event injection with per-job threading.Event. See Cancellation docs.
  • Progress callbacks with four dispatch paths — Async/sync callbacks work with or without an event loop. See Progress Callbacks docs.
  • Lazy event loop resolutionQuiv() can be instantiated at module level before any asyncio loop exists. The event loop is resolved on first progress callback dispatch.
  • Backpressure — Scheduler defers dispatch when the thread pool is full. Late-starting jobs log a warning suggesting to increase pool_size.
  • TaskStatus.RUNNING — Tasks are marked RUNNING during execution, preventing concurrent runs of the same task.

Bug Fixes

  • Removed logger.setLevel(logging.DEBUG) — the library no longer forces a log level
  • Removed asyncio.get_event_loop() at init — fixes deprecation warnings and module-level instantiation
  • shutdown() now cleans up SQLite WAL (-wal, -shm) sidecar files
  • Cancellation detection no longer depends on handler accepting _stop_event

Improvements

  • TaskStatus / JobStatus are now proper (str, Enum) classes
  • History cleanup uses SQL-level filtering with col() wrapper (runs every 60s, not every tick)
  • Next run scheduled from job completion time, not dispatch time
  • All log timestamps use the configured display timezone consistently

Documentation

  • New pages: Bigger Applications, Progress Callbacks, Cancellation (with mermaid diagrams)
  • Architecture page now has a sequence diagram
  • API page: added pool size guidance, logger/timezone note blocks
  • Tabbed uv/pip install commands across all pages
  • GitHub repo link in docs header with edit/view source buttons

Tests

  • 8 new tests covering backpressure, late start warnings, concurrent run prevention, remove_task, progress callbacks without event loop, and task lifecycle
  • All existing tests updated for API changes

Repository

  • Added CODEOWNERS, issue templates, CONTRIBUTING.md, CODE_OF_CONDUCT.md
  • CI workflows now include pyproject.toml in path filters
  • Removed master branch reference from docs deploy workflow

Changes

Initial Release (v0.1.0) - 2026-03-08

Initial Release

Changes