Files
logseq/docs/recurring-tasks.md
Cory Donnelly 2169a86f03 fix: honor documented repeater cookie semantics (#12523)
* fix: honor the three documented repeater cookie semantics

Logseq's documented repeater semantics (per docs.logseq.com and
`logseq/docs` `Tasks.md`) define three org-mode-style cookies for
recurring tasks:

  `.+`: repeats from the last completion date
  `++`: advances from scheduled, skipping in whole intervals to future
  `+`:  advances from scheduled by the declared interval (can stack)

The scheduler in `worker/commands.cljs` has been ignoring the cookie
entirely and applying a single, `++`-like semantic for every
recurring task. A user who wrote `.+1w` in markdown — expecting "a
week from when I actually finished" — silently got `++1w` behavior
("a week from the original scheduled date, skipped to future"),
which for a weekly task scheduled 2026-04-01 and completed on
2026-04-05 returns the next occurrence on 2026-04-08 rather than
the documented 2026-04-12.

This change:

  * Adds a closed-values `:logseq.property.repeat/repeat-type` property
    with values `:dotted-plus` / `:plus` / `:double-plus`. Default is
    `:double-plus` so existing recurring tasks see no behavior change
    on upgrade.
  * Rewrites the scheduler to branch on repeat-type and implement each
    semantic: `.+` anchors on now; `+` advances from original once (can
    stack overdue, per org-mode); `++` iterates in whole intervals
    until strictly after now. The `++` path is mathematically
    equivalent to the previous scheduler, so default-path behavior is
    preserved.
  * Guards against frequency <= 0 — the old code would silently produce
    nonsense and, under the new `++` loop, would spin forever. The
    guard short-circuits to `nil`.
  * Extracts `resolve-recur-frequency` and fixes the previous
    `(or [A B] [C D])` pattern in `compute-reschedule-property-tx` —
    any 2-vector is truthy in Clojure, so the default-value branch
    was unreachable and entities without an explicit
    `:recur-frequency` silently fell through to `frequency = nil`.
    `if-let` makes both branches reachable so default-to-1 actually
    works at migration time.
  * Restores the cookie-type selector that was removed from the
    date-time popover in `0a5b88467` (Nov 2020) — in-code support for
    all three cookies has been present but not user-pickable for the
    last ~5.5 years.
  * Adds `docs/recurring-tasks.md` — a technical spec for contributors
    and users that restates and augments the upstream `Tasks.md` text,
    adds decision guidance, and documents the implementation surface.
  * Extends the file-graph → DB-graph migration (built on top of
    `44d6bd49c4` "fix: preserve repeated schedule import") to also
    carry the cookie kind via a new `repeat-types` map in
    `graph-parser/exporter.cljs`, so an imported `.+1w` task lands in
    the DB-graph with `:repeat-type.dotted-plus` rather than picking
    up the `:double-plus` default. Test updated to assert this.
  * Adds deftests covering each cookie's distinctive behavior plus
    boundary cases (non-positive frequency, unknown unit, frequency > 1
    variants, `++` at month/year units, and both branches of
    `resolve-recur-frequency`).

The preexisting `get-next-time-test` passes unchanged under the
`:double-plus` default, preserving the existing regression contract.
Tests pin `t/now` via `with-redefs` for determinism.

Refs #7731, #11260, #6715, #8531. Folds in the small remaining delta
from #12532 (now closed as superseded by `44d6bd49c4`).

* fix: harden recurring task repeat type

* fix: contain repeat type selector

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix: handle clamped monthly repeats

---------

Co-authored-by: Tienson Qin <tiensonqin@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-14 17:53:11 +08:00

130 lines
4.9 KiB
Markdown

# Recurring Tasks
Logseq supports recurring tasks with three scheduling semantics, modeled
on org-mode's three repeater cookies. Picking the right one lets you
say exactly how the next occurrence should be computed when you mark
a task DONE.
This document is the reference for contributors working on the
scheduler and for users who want the full behavior specification. The
short, user-facing version lives in
[logseq/docs `Tasks.md`](https://github.com/logseq/docs/blob/master/pages/Tasks.md).
## The three semantics
### `.+` — Advance from completion
> *Repeats from the last time you marked the block done.*
The next occurrence is one interval after the moment you marked the
task DONE, regardless of what the original scheduled date was.
**Best for:** habits and cadence-driven recurrences — things where the
clock restarts the moment you finish.
**Example:** A weekly task scheduled for 2026-04-01, completed on
2026-04-05, will next appear on 2026-04-12. If you'd forgotten and
completed it on 2026-04-20, it would next appear on 2026-04-27. The
interval is anchored to your completion, not to the calendar.
### `++` — Advance from scheduled, skip to future
> *Keeps it on the same day of the week.*
The next occurrence starts at *original + one interval* and keeps
advancing in whole intervals until the result is strictly in the
future. Because the arithmetic is in UTC and advances by whole weeks
(or months, or years), weekly recurrences naturally land on the same
day-of-week as the original.
**Best for:** calendar-anchored recurrences where completion is a side
event and the anchor matters — "every Monday standup," "monthly review
on the first of the month."
**Example:** A weekly standup scheduled for Monday, 2026-04-06:
- Completed Wednesday, 2026-04-08 → next occurrence Monday, 2026-04-13.
- Completed Friday, 2026-04-17 → next occurrence Monday, 2026-04-20.
**This is the default** when a recurring task has no explicit cookie.
### `+` — Advance from scheduled, stacking
> *Repeats in X y/m/w/d/h from when you originally scheduled it.*
The next occurrence is exactly *original + one interval*. If
completion was much later than the original date, the next occurrence
may land in the past — which causes the task to appear overdue
immediately. Org-mode calls this "stacking."
**Best for:** obligations tied to an exact calendar date that should
not shift when you're late — monthly rent, annual renewals, fixed
billing cycles.
**Example:** Rent due 2026-04-01, paid 2026-04-05 → next reminder
2026-05-01. Paid 2026-04-25 → next reminder still 2026-05-01.
## Choosing the right cookie
A decision table for picking the right semantic:
| Intent | Cookie |
|---|---|
| "Every 7 days from when I last did it" | `.+` |
| "Every Monday" | `++` |
| "The 1st of every month" | `+` |
## Default and backward compatibility
Recurring tasks without an explicit `repeat-type` default to `++`.
This preserves the behavior that existed before the three cookies were
honored independently; no existing task changes on upgrade. Users who
want `.+` or `+` semantics can pick them via the repeat-setting
popover on the task.
## Implementation reference
### Scheduler
The scheduler lives in `src/main/frontend/worker/commands.cljs`. It
dispatches on `repeat-type` via `repeat-next-timestamp`:
- `advance-from-completion` implements `.+`
- `advance-from-scheduled` implements `+`
- `advance-until-future` implements `++`
The dispatch falls back to `++` for `nil` or unknown values, so the
scheduler is safe against missing or future-added cookie types.
Frequencies of zero or less short-circuit to `nil` before dispatch to
avoid infinite loops.
### Property
`:logseq.property.repeat/repeat-type` is a closed-values property
declared in `deps/db/src/logseq/db/frontend/property.cljs`. Its closed
values are:
- `:logseq.property.repeat/repeat-type.dotted-plus``.+`
- `:logseq.property.repeat/repeat-type.plus``+`
- `:logseq.property.repeat/repeat-type.double-plus``++` (default)
### UI
The repeat-setting popover in
`src/main/frontend/components/property/value.cljs` exposes a "Next date"
selector bound to the `repeat-type` property. It appears whenever the
task's repeat checkbox is enabled.
### Tests
Scheduler tests live in
`src/test/frontend/worker/commands_test.cljs`. Each of the three
semantics has a dedicated `deftest`; the preexisting
`get-next-time-test` exercises the default (`++`) path. Time is pinned
via `with-redefs [t/now ...]` for determinism.
## Related reading
- End-user docs: [logseq/docs `Tasks.md`](https://github.com/logseq/docs/blob/master/pages/Tasks.md)
- Org-mode spec: [Repeated tasks](https://orgmode.org/manual/Repeated-tasks.html)
- Tracking issues: [#7731](https://github.com/logseq/logseq/issues/7731), [#11260](https://github.com/logseq/logseq/issues/11260), [#6715](https://github.com/logseq/logseq/issues/6715), [#8531](https://github.com/logseq/logseq/issues/8531)