Files
xarmian b721dfb74d feat(schedule): workspace agenda endpoint (TASK-4104 D1) (#2357)
* feat(store): date-window item filter and one shared visibility clause (TASK-4104 D1)

ItemListParams.FieldDateWindow keeps items where any named date field falls
on a day in a window, on both stored date shapes and both dialects. The
pending-reminder visibility switch becomes scopeClause so the agenda reads
the same predicate.

Claude-Session: https://claude.ai/code/session_01Pa22hHpfXoZbQkEFvPtNeS

* feat(schedule): workspace agenda endpoint (TASK-4104 D1)

GET /workspaces/{ws}/agenda?from&to&tz lists the schedules' occurrences with
their state and the items dated in the window, in the caller's zone, under
the dashboard's visibility applied in SQL. A per-schedule cap degrades that
schedule to truncated or unavailable instead of failing the read. History
rows merge by wall for floating and all-day schedules, by instant for pinned
timed ones.

Claude-Session: https://claude.ai/code/session_01Pa22hHpfXoZbQkEFvPtNeS

* test(schedule): classify the agenda in the compress census and the lifecycle guard (TASK-4104 D1)

Claude-Session: https://claude.ai/code/session_01Pa22hHpfXoZbQkEFvPtNeS

* fix(schedule): agenda scan skips ended schedules, one bad schedule is unavailable, empty tz is refused (TASK-4104 D1 r1-1, r1-2, r1-5)

- r1-1: the 1000-schedule scan bound no longer counts schedules that ended
  before from (ended_at IS NULL OR ended_at >= from).
- r1-2: a non-cap error from one schedule's expansion is logged by schedule
  id and error class only and listed under unavailable with code internal;
  the agenda stays 200.
- r1-5: an explicit empty or blank tz is 400 invalid_tz.
- Tests: scan starvation, corrupt schedule, empty tz, the 100 and 500 cuts,
  an app-token ceiling vs the item list, a pinned HOURLY and a floating
  missed count across New York's 2026-11-01 fall-back, a stronger runaway row.
- Docs: date-entry selection and at, the cut before date ordering, accepted
  deviations.

Claude-Session: https://claude.ai/code/session_01Pa22hHpfXoZbQkEFvPtNeS

* fix(schedule): agenda lists an early-resolved schedule, store classifies expansion errors (TASK-4104 D1 r2-1, r2-2)

r2-1: an ended schedule stays in the agenda scan while a history row can
reach [from-36h, to+36h), so a deadline marked done early still lists as
done; one whose history is wholly outside the window still spends no bound
slot. ORDER BY is qualified: the unnamed reminder subselects also output an
"id" column, which Postgres refuses as ambiguous.

r2-2: ExpandAgenda reports unparseable stored state as unavailable
"internal" with a nil error and returns an error only for a database
failure; the handler answers that with a 500 again. The 1.5 s budget is
checked between schedules and never reaches that branch.

r2-3: the API docs state the three behaviours under a plain label.
r2-4: the runaway test proves the runaway's own ref and the healthy refs.

Claude-Session: https://claude.ai/code/session_01Pa22hHpfXoZbQkEFvPtNeS

* test(schedule): the agenda's DB-failure row breaks only the history read; the runaway row pins its one outcome (TASK-4104 D1 r4-2, r4-4)

TestAgendaDatabaseFailureDuringExpansionIs500 dropped schedule_occurrences,
which since r2-1 also fails the list query's EXISTS, so it 500ed before
ExpandAgenda and could not catch a handler that swallowed the error. It now
renames a column only agendaOccurrences selects. A mutant that swallows the
error as unavailable=internal turns it red.

TestAgendaOneRunawayScheduleDoesNotTakeItDown accepted either outcome. A
MINUTELY rule over the window hits the instance cap with partial
occurrences, cut to the per-schedule cap: exactly 100 entries, truncated.
It pins that; AgendaMaxPerSchedule 100 -> 99 turns it red.

Claude-Session: https://claude.ai/code/session_01Pa22hHpfXoZbQkEFvPtNeS
2026-10-31 09:32:23 -04:00
..