EverTask 4.0: durable occurrences, misfire policies, time zones, schedule management, exclusions - #55
Merged
Conversation
…r fixture (#23) - X6/P6 rev. v1.1: next release is 4.0.0 - the major is forced by the Abstractions namespace flattening already on master (62d2010, breaking, no type forwarders), not by #23; the #23 changes stay additive - consumer compatibility fixture redefined: compiled against the issue23-baseline packages (local nupkg feed) instead of the published 3.11 nupkg, which can no longer load post-62d2010 - golden-byte JSON fixtures stay anchored to the 3.11 wire format (persisted-data compatibility is independent of the recompile) - orchestrator lenses/titles updated accordingly; development runs on the feature/issue23-durable-occurrences branch, PR to master Claude-Session: https://claude.ai/code/session_0133AHLGT4SoFa4JWtkZFuQj
…inistico, compatibilita, (#24) - Introduce the IScheduleEvaluator/ScheduleEvaluator seam and route every next-run computation (dispatcher, worker, schedulers, recurring builders) through it - Make scheduling deterministic (P9): TimeProvider flows through schedulers, rate limiting and the recurring builders, with FakeTimeProvider-driven tests - Preserve JSON/record compatibility and add the OccurrenceMode scaffold plus a binary-compatibility baseline consumer fixture and golden-JSON tests - Add the RecoveredTaskFactory and split recovery into execution vs finalization (X3), with the two-pass recovery page and finalization-failure handling - Ship one AddDurableOccurrences migration per provider (SqlServer, Postgres, MySql, Sqlite) with the durable-occurrence columns, constraints and procedures - Implement atomic storage operations (materialize occurrence, schedule CAS, cancel/requeue/halt) at each provider's optimization tier, plus in-memory parity
…ntext) (#25) - add ITaskExecutionContext, MisfireInfo/MisfireKind and ITaskExecutionContextAccessor to EverTask.Abstractions, with a no-op SetExecutionContext DIM on IEverTaskHandler<T> (non-breaking) and a protected Context on EverTaskHandler<T> - build one context per delivery in WorkerExecutor.DoWorkCore, publish it to the singleton ambient AsyncLocal accessor and inject it through delegates compiled once per handler type (the same cache now also carries SetLogCapture, previously per-execution reflection) - track Attempt across the retry loop and RunNumber durably (stamped by the dispatcher as CurrentRunCount + 1, advanced by QueueNextOccourrence) with no extra storage round-trip - resolve the nominal slot through TaskHandlerExecutor.NominalSlotOfDelivery so a rate-limit deferral moves the delivery but never the slot the handler and dashboard see - add SetMisfireThreshold(TimeSpan) (default 5s, observation only) driving Misfire.Kind = Late - update docs, cheatsheet, reference and the integrate-evertask skill; add execution-context and eager-scope integration tests
- Add InTimeZone(TimeZoneInfo|string) as default interface members on the builder interfaces, implemented by all seven internal builders; the zone is persisted as a normalized IANA id in the schedule JSON (RecurringTask.TimeZoneId, [JsonIgnore(WhenWritingNull)], no new column). - Introduce ScheduleSemantics to classify each schedule as Calendar or Elapsed, so that only calendar-anchored schedules are governed by a time zone. - Add WallClock to map a nominal wall slot to an instant with the T6/T7 rules: exponential bracket plus tick bisection for a DST gap, first pass for a repeated hour, and Consumed so collapsed slots fire exactly once. - Delegate cron to Cronos with the zone and wire Cronos in as the test oracle. - Add SetDefaultScheduleTimeZone, stamping calendar schedules at dispatch, and expose TimeZoneId / ScheduledAtLocal on the task execution context. - Keep the legacy path byte-identical: GoverningZone short-circuits on a null id before any new code runs; docs, cheatsheet, reference and the integrate-evertask skill updated, plus the new time-zones test suite.
- OccurrenceMode.Durable: la riga schedule diventa definizione + cursore; OccurrenceMaterializer crea una riga figlio per slot dovuto (RunAsync idempotente, lock per-padre, budget globale, kick non-throwing, seconda ondata della recovery) - Misfire policies tipizzate: OnMisfire(Skip/FireOnce/CatchUp), CatchUpOptions (MaxAge, MaxOccurrences con Halt/SkipOldest, MaxPendingOccurrences), WithDurableOccurrences, BackfillFrom - DueSlotEnumerator: conteggi bounded, SetMisfireThreshold applicato alla materializzazione, Halt solo se il budget residuo supera il cap, perdite tipizzate per causa (finestra 1802 / overflow 1819 / skip 1820) - Storage: TryAdvanceScheduleCursor, TryHaltSchedule, retention delle occorrenze, parità SqlServer/Postgres/MySql/Sqlite/Memory; recovery R8 con seconda scansione keyset - Test: DueSlotEnumeratorTests, DurableOccurrencesIntegrationTests, CatchUpRecoveryIntegrationTests (SQLite), multi-host SqlServer, validazioni negative, scheduler senza SupportsScheduleInspection - Docs, cheatsheet, reference, skill integrate-evertask, CLAUDE.md di modulo; LoadHarness con celle LRA/LDM; decisioni §3.5 (ratifiche di fase 4) Claude-Session: https://claude.ai/code/session_01DR6CCxkRRsZmPZhoTz79Sr
- add ITaskScheduleManager to EverTask.Abstractions (Reschedule, ReevaluateSchedule, ResumeSchedule, RequeueFailedOccurrence, CancelSchedule) with RescheduleMode and ScheduleUpdateResult - implement TaskScheduleManager and its log class (EventId 2200-2299), extracting the dispatcher per-taskKey critical section into TaskKeyLockRegistry so dispatch and reschedule of the same key share it - add ScheduleVersionRegistry (S4) plus SchedulePeriodKind, ScheduleRebase and RecurringTask.PeriodKind for the M18 nominal-period rebase - WorkerExecutor drops stale inline deliveries and advances the schedule with a compare-and-swap that re-aims on VersionMismatch - no storage members or migrations needed: phase 1 already shipped UpdateSchedule, RequeueTerminal and the CAS overloads at each provider optimization tier, so the legacy advance path stays byte-identical - update docs, cheatsheet, reference, the integrate-evertask skill, the ASP.NET sample and four CLAUDE.md files; 31 new tests (21 integration, 10 rebase unit)
- add the public INextOccurrenceProvider contract, NextOccurrenceRequest and OccurrenceProviderException, plus a key-based registry resolved per call in its own DI scope - expose UseOccurrenceProvider on the fluent recurring builder and route every schedule primitive through a provider branch behind IScheduleEvaluator, so misfire policies, durable occurrences, time zones, skip-forward and ITaskScheduleManager work over a provider unchanged - implement the V4 failure model: an unknown key is a configuration error at dispatch and a terminal poison at recovery, while a provider exception is transient (nothing written, backoff re-park, warning event, recovery poison counter untouched) - add SetOccurrenceProviderRetry to configure the transient backoff, with its own retry registry and provider logging - document the feature end to end (new occurrence-providers page, cheatsheet, configuration reference, integrate-evertask skill) and ship a BusinessDaysProvider sample - cover the phase with 37 new tests; full suite green on net8/net9/net10 with 0 warnings
…les, release 4.0.0 (#30) - monitoring api: durable-occurrence facts on TaskListDto/TaskDetailDto as init properties, new GET /tasks/{id}/occurrences backed by ITaskStorage.GetOccurrences, occurrence/catch-up filters, TaskCountsDto.Occurrences and OverviewDto.CatchUpBacklog, with TaskScheduleFacts as the single JSON-column reader - dashboard ui: mirrored TS types, occurrences tab on durable schedules, catch-up/fire-once/late badges, halt alert and the catch-up backlog KPI on the overview - docs and skills sweep: monitoring events/api/dashboard pages, architecture, recurring-tasks, index and README refreshed; every "3.12.0" marker rewritten to 4.0.0; .agents mirror and integrate-evertask skill resynced - samples: NightlyReconciliationTask (02:00 Europe/Rome, 92-day catch-up) plus schedule/backfill/resume reconciliation endpoints - release: CHANGELOG [Unreleased] cut as [4.0.0] - 2026-08-26, with the missing phase 4/5/6 entries written and stale cross-references repointed - tests: OccurrenceEndpointTests on a real host with the real dispatcher and materializer, plus API-reference sample and wire-contract tests
…ntry points (#37) - QueuedTask.NormalizeTimestampsToUtc rewrites every timestamp to the same instant at offset zero - applied in Persist/UpdateTask of the EF base (inherited by all four relational providers) and MemoryTaskStorage - SQLite stores DateTimeOffset as TEXT with the offset inside: a row written at +02:00 lost every cursor CAS forever - contract documented on ITaskStorage for custom storages; CLAUDE.md notes updated - pinned by two cross-provider contract tests (red without the fix on MaterializeOccurrence/TryHaltSchedule) Closes #37 Claude-Session: https://claude.ai/code/session_01DR6CCxkRRsZmPZhoTz79Sr
) - both poison primitives are best-effort: the recovery summary inferred terminalization from the call returning - TryPoisonAsync re-reads the row and measures it against IsRecoverableForExecution + IsRecurringSeriesToFinalize - a poison write that throws is contained (no longer aborts the whole recovery wave); shutdown OCE rethrown - honest accounting: unconfirmed poison counts as transient, not permanent; per-site poisoned logs gated on confirmation - EventIds 1131/1132; pinned by RecoveryPoisonOutcomeTests (real MemoryTaskStorage behind FaultInjectingTaskStorage, 3/4 red without the fix) Closes #38 Claude-Session: https://claude.ai/code/session_01DR6CCxkRRsZmPZhoTz79Sr
…d idle queues (#39) - the reader no longer awaits each page's wave: up to MaxRecoveryPagesInFlight (4) waves run at once - a slot is freed by whichever wave finishes first, so one wedged page costs one slot, not the pipeline - gate sits before the page read (memory stays bounded by a constant, R8); M7 barrier drains every wave before the second pass - a failing wave still ends the recovery; the waves in flight are settled, not abandoned - same pipeline on the durable second scan; pinned by RecoveryPagePipelineTests (red at cap 1 = pre-fix behaviour) Closes #39 Claude-Session: https://claude.ai/code/session_01DR6CCxkRRsZmPZhoTz79Sr
- a handler that never builds burned no counter: the row stayed non-terminal for the process lifetime and froze the series under MaxPendingOccurrences=1 - reuses the recovery's L18 counter (IncrementRecoveryFailure) with the same default ceiling of 5 consecutive failures; a successful rebuild clears it - at the ceiling the row goes through the same confirmed FailUnusableOccurrenceAsync, with its own sentence (EventId 1825 + monitoring event); 1817 now says attempt N of M - RequeueTerminal resets the counter so a requeued row gets its attempts back (all providers via the EF base) - pinned by two integration tests (red without the fix at DurableOccurrencesIntegrationTests.cs:750/:801) Closes #41 Claude-Session: https://claude.ai/code/session_01DR6CCxkRRsZmPZhoTz79Sr
…nd shared probes (#40) - real root cause: the host's startup recovery captures its cutoff after StartAsync returns, so a row dispatched in that window is legitimately re-dispatched (one extra handler resolution) - order-dependent, not the counters alone - the resolution-counting test now waits for the recovery's terminal log line (new shared StartupRecoveryWatch helper) - the two absolute-count tests get a dedicated probe pair each (the issue's ask); assertions unchanged Closes #40 Claude-Session: https://claude.ai/code/session_01DR6CCxkRRsZmPZhoTz79Sr
… it (#43) - three tests read the row's counter right after the handler's own counter, but the worker writes CurrentRunCount a storage round-trip later - they now wait on WaitForRecurringRunsAsync (audit + counter); an always-true guard became an explicit ShouldBe(2), so the interval assertion always runs - a counter never written still fails the wait: nothing weakened Closes #43 Claude-Session: https://claude.ai/code/session_01DR6CCxkRRsZmPZhoTz79Sr
… the engine (#45) - write pressure moves into T-SQL loops over the real usp_SetTaskStatus (client thread-pool starvation under a full-solution run kept the cycle from forming) - stop stays between bursts; early stop on a failed read; engine deadlock counter (DMV) read before/after so the failure message tells no-collision apart from a storage that stopped re-running reads - still fails when nothing collides: readFailures/reread assertions untouched (no dynamic skip in xUnit 2.x without a new package) Closes #45 Claude-Session: https://claude.ai/code/session_01DR6CCxkRRsZmPZhoTz79Sr
… model (#42) - second role (read vs operate) on the existing JWT: only the new ManagementUsername/Password credential grants operate; dashboard credential and magic link stay read - gate lives in the auth middleware on the /api/management prefix: 404 when EnableManagementEndpoints=false (default, backward compatible), 401/403 otherwise; ManagementAuthorization hook replaces the role check - auth disabled does not open the write surface (403 unless the host hook says otherwise); CSRF ruled out and documented (Bearer-only, no cookies) - POST tasks/{id}/requeue|resume|cancel over ITaskScheduleManager (optional: 501 standalone); outcome-to-HTTP mapping in one place; fixed-time credential compares - 14-test authorization matrix incl. real cancel against storage; OpenAPI hides the routes while disabled; docs triad + api reference updated Closes #42 Claude-Session: https://claude.ai/code/session_01DR6CCxkRRsZmPZhoTz79Sr
- two new ITaskStorage DIMs (GetStatusAuditsPage/GetRunsAuditsPage -> AuditPage<T> with TotalCount); existing signatures untouched, defaults compose the unpaged read for custom storages
- indexed overrides: EF base (Count + Skip/Take on the QueuedTaskId index, identity order = insertion order, take=0 answers only the total), Memory, SqlServer deadlock re-read
- endpoints take skip/take like /execution-logs and answer {audits, totalCount, skip, take}; task detail carries the first page + both totals
- UI: one AuditTrailTab for both trails with Previous/Next, reads the endpoints; SignalR invalidation by query-key prefix
- pinned end-to-end: 7 endpoint tests, cross-provider storage tests (4 providers + memory + real SQLite), 6 Vitest UI tests; pnpm build/lint/test green
Closes #44
Claude-Session: https://claude.ai/code/session_01DR6CCxkRRsZmPZhoTz79Sr
…nly chain (#34) - mirrors the runtime classification (IsCalendarAnchored), not the fluent names: Day/Week/Month intervals are calendar on their own, OnHours() classifies as elapsed - proof-based receiver walk: unrecognized call, split chain, foreign type or a second chain in scope aborts - zero false positives by construction - warning only: the runtime InvalidOperationException stays the contract - 34 analyzer tests with three-mutation red/green evidence; docs (time-zones.md, CHANGELOG, skill) and analyzer range updated to ET0010 Closes #34 Claude-Session: https://claude.ai/code/session_01DR6CCxkRRsZmPZhoTz79Sr
…orward the audit pages in the fault wrapper - the WorkerService registration became a factory lambda, so removal by ImplementationType silently stopped matching: the worker ran in every worker-off test and its recovery re-parked the seeded completed recurring row mid-test (timing-dependent TotalCount 30 vs 31) - the filter now matches the lambda return type too and THROWS when nothing is removed, so it cannot regress silently - FaultInjectingTaskStorage forwards GetStatusAuditsPage/GetRunsAuditsPage (its own contract test caught the gap) Claude-Session: https://claude.ai/code/session_01DR6CCxkRRsZmPZhoTz79Sr
…re-auth hook evaluation (#42) - the authoritative gate is now an IAsyncAuthorizationFilter attached by route convention to everything under {prefix}/api/management: it runs inside routing (after UsePathBase) and after the host's UseAuthentication, so an anonymous request through a path base can no longer reach the actions and the ManagementAuthorization hook finally sees the authenticated principal - the middleware keeps only the 404 shield for the disabled surface; the filter re-validates the bearer on its own (header only, never the query-string fallback) - startup validation refuses a management password equal to the read password or the magic-link token (and half-configured pairs) - 4 new tests (PathBase bypass dead with the row proven untouched, hook on the host principal, credential distinctness); the original 14-test matrix unchanged and green Hardens the fix for #42 after the consolidated adversarial review of the fix series. Claude-Session: https://claude.ai/code/session_01DR6CCxkRRsZmPZhoTz79Sr
…se (#46) - one MonitoringAccessPolicy (surface classification, IP whitelist with CIDR, JWT) judged on the path routing resolved, after the path base - enforcement inside routing: MonitoringAccessFilter on every monitoring controller (order -1000, before the management gate) and an endpoint guard wrapping the non-MVC endpoints (SignalR hub, dashboard files, OpenAPI, companion packages) via an empty-prefix route group - the middleware shrinks to an outer shield; host endpoints are never wrapped (pinned by test); behavior identical without a path base (existing matrix green unchanged) - 7 new tests: reads/UI-IP/hub all answered 200 anonymously under a path base before the fix, now 401/403 with the row/behavior proven; anonymous auth paths and authenticated hub handshake still work - known functional (non-security) limit documented: the CORS branch still matches pre-path-base Closes #46 Claude-Session: https://claude.ai/code/session_01DR6CCxkRRsZmPZhoTz79Sr
- the client address is Connection.RemoteIpAddress, nothing else: a spoofed header carrying a whitelisted address no longer walks through (the dashboard files had no other layer) - reverse-proxy hosts use the framework's answer: UseForwardedHeaders with KnownProxies/KnownNetworks rewrites the address before routing - documented in reference/cheatsheet/skill with a BREAKING note in the 4.0.0 CHANGELOG (plus a sober Fixed line for #46) - all access decisions now live inside routing (the middleware keeps only the 404 shield): the host's UsePathBase and UseForwardedHeaders have both acted before the policy judges - 5 new tests: spoof refused on API and dashboard (200 pre-fix), the documented proxy path works with a known proxy, a stranger forwarded by a trusted proxy still refused, plain connection address admitted Closes #47 Claude-Session: https://claude.ai/code/session_01DR6CCxkRRsZmPZhoTz79Sr
- A stranded occurrence is permanently Failed after ~5 minutes of a transient handler-activation failure, because the rebuild ceiling is spent per operational retry instead of per restart - RebaseFromCursor silently skips the pending occurrence and re-phases a monthly schedule that names several days - BackfillFrom on a month cadence with OnDays starts the cursor past a listed day it was pointed at - RecurringInfo renders RunUntil in the HOST machine's local time and then labels it with the schedule's time zone - Occurrence retention cascade-deletes StatusAudit/RunsAudit rows inside their own configured window - CHANGELOG 4.0.0 claims "The API stays read-only" while the same release ships three write endpoints, and has no entry for #42 or #44 - EverTask.Monitor.Api package README (the nuget.org page) still asserts every endpoint is read-only and omits the whole management surface - docs/storage/custom-storage.md never states the two new obligations a 4.0 custom ITaskStorage has (UTC normalization on Persist/UpdateTask, clearing the failure counter in RequeueTerminal) - Plugin marketplace entry is frozen at 1.1.1 across the whole 4.0.0 rewrite and still advertises analyzers ET0001-ET0008 - 3,270 lines of new #23 tests live in test projects CI never executes - CI excludes the SQL Server and PostgreSQL runs of the four-provider storage contract suite - Every call of the five new atomic storage operations in the contract suite passes AuditLevel.Full, so the non-audited branch is untested on a
- Redispatch revival removes the blacklist entry that was covering in-flight cancelled occurrences, which then execute. - A cancelled-then-redispatched durable schedule keeps its Halted marker and never materializes again. - A policy that prunes only one audit trail turns OccurrenceRetentionDays into a permanent no-op. - Reviving a cancelled schedule drops the blacklist entry that is the only guard for its in-flight occurrences.
- Reviving a cancelled INLINE schedule uncovers its own in-flight delivery, which runs the cancelled series and evicts the new registration - The revival's WaitingQueue write is best-effort: a swallowed failure strands the redispatched series terminally Cancelled behind a success answer - The revival's un-cancel is a best-effort SetStatus whose failure is swallowed, and the log still reports the series restarted - A re-dispatch under the task key rewrites a Cancelled schedule row to Completed, erasing the cancellation and stranding the cancel's blacklist entry
…le event shapes (#23) Post-final-review fix series, part 4 of 4 (documentation lens, plus the maintainer decisions record). - The storage guides no longer recommend "scale-out / multi-instance" deployments the runtime does not support: SQL Server/PostgreSQL rows now say high write concurrency on ONE active host per store, standby allowed, and point at the scalability page (integrate-evertask skill, postgres/mysql/ sqlite/overview pages, configuration reference). - TrySetTerminalOutcome joined every place that enumerates the versioned ITaskStorage contract: custom-storage guide, integrate-evertask skill, CHANGELOG 4.0.0 operation list. - monitoring-events.md documents both stable shapes of the occurrence-unusable message and the failed-revival Error event, so consumers can alert on all of them. - SetMisfireThreshold XML and release note no longer claim delivery-time-only effect: the durable planner consumes it too. - RecurringTask.ToString appends RunUntil/MaxRuns on the cron branch like the fluent and provider branches (regression test included in part 1's suite). - The integrate-evertask "every knob" example gains the three scheduling knobs it omitted. - CHANGELOG: BREAKING entries for the ratified removals of ITaskStorage.GetCurrentRunCount and TimeOnly.ToUniversalTime. - decisions.md §3.6 records the post-final-review maintainer ratifications (removals, CI exclusion, perf findings deferred to issues #48-#54 with the composite index pulled in-branch).
The ConsumerCompatibility.Baseline fixture, the local nupkg feed, NuGet.config and the baseline-driven tests were development inputs: the additivity proof ran in the workflow gates and the final review, and CI only needs the shipped code green. The permanent guards stay: the golden-byte JSON fixtures and the LegacyMinimalTaskStorage compile pin. Recreate a fixture against the published 4.0.0 packages if a 4.x minor ever needs the binary proof again; the issue23-baseline git tag remains for history.
review/recurring-occurrences-final-report.md replaces the synthesis the aborted final review never ran: per-phase outcomes, review and fix-series history, test totals on net8/9/10, perf versus baseline, known limits, verdict GO for 4.0.0. The two limits the review left undocumented are now written down: 4.0 is an upgrade boundary for durable schedules (no 3.x rollback while children exist; the Down migration deletes them by design), and a custom IScheduler that keeps the TrySchedule default cannot refuse a stale in-flight registration (CHANGELOG, durable-occurrences guide, IScheduler XML).
… storage docs pruned README leads with what 4.0 actually changes — durable occurrences with observable misfire handling, execution context, runtime schedule management — without dropping anything it already covered: the analyzer table (stale at ET0007) becomes a paragraph with a link, the bottom NuGet list goes (the badges already carry it), the roadmap section shrinks to a pointer. ROADMAP.md is rewritten from its 3.2-era state: planned items in plain words, shipped milestones condensed, no effort estimates and no references to internal files. The docs touched by #23 get a humanizer pass: promotional fillers and AI-pattern phrasing removed, technical content, tables and event texts untouched. The three new recurring pages needed nothing. The user-facing storage pages also drop their schema internals (column tables, index and procedure names, CAS mechanics) — users need behavior and configuration, and migrations apply themselves; custom-storage.md keeps the ITaskStorage contract, restated engine-neutral.
…tion sets The certification and review cycles grew these files into design diaries: the core file alone reached 775 lines of narrated proofs, keyed by review-plan sigles (S4, M7, P5, ...) that reference documents which will leave the repo. Every operative invariant survives as a 1-3 line bullet stating the constraint and its why in plain words; the narratives die here — their content already lives in the decisions file and the commit history. The core file lands at 397 lines, above the usual 40-100 budget on purpose: it is the declared home of the queue/recovery no-loss invariants and what remains is all normative. The other six oversized files land inside the budget. Stale references were corrected against the current code (TrySetTerminalOutcome added, GetCurrentRunCount and the unpaged audit members gone).
…xtensions with tests The monitoring-logs guide showed the raw storage-contract member (GetExecutionLogsAsync with skip/take) as the way to read persisted logs; the paged GetLogsAsync extensions are the intended consumer surface. The guide now uses them and notes where the raw member belongs (custom storage). TaskStorageExtensions had no tests: added coverage over the real MemoryTaskStorage for ordering, paging math, argument validation and the page-size cap.
…ner separator The AspnetCore sample now registers two recurring demos that exercise the 4.0 surface end to end: a durable every-20-seconds schedule with MaxRuns, and a nightly Europe/Rome schedule backfilled three days into the past with a CatchUp policy — the pair the dashboard verification ran against. The task-detail halt banner concatenated the API's Reason fragment (no trailing period, by contract) straight into the next sentence, rendering "…cap At least N slots…". The banner now inserts the separator itself.
Comments now say WHY in 1-3 lines or not at all: development-plan sigles, review narratives, proof-of-correctness essays and test-evidence notes are gone; what survives is the invariant markers, the XML docs on the public surface and the GitHub issue references. MediatR attribution comments are untouched. No code change — verified by a clean Release build and the full Docker-free test suite. Also fixes three ambiguous crefs the sweep exposed.
The description now names what 4.0 actually ships (durable occurrences, misfire policies, time zones, rate limiting, dashboard) and the tags cover the searches that should find it.
The per-phase adversarial reviews and the followups registry served the development run; every followup entry is now a GitHub issue, so the tracked record of #23 stays at the three durable documents. The untracked files remain on disk, ignored like the other local review artifacts.
…sarial review rounds
…n, serialization (#36) First slice of the fixed-exclusions feature: ScheduleExclusions/ExclusionRange beside the definition, the Except/ExceptWeekends fluent surface as default interface members with per-builder redeclarations, canonical normalization in Validate (sorted/deduped days and dates, UTC-normalized merged half-open ranges, 1000-entry cap, all-seven-days and provider-coexistence refusals), the relaxed Elapsed-zone rule for calendar exclusions with the default zone stamped at ingress, and the bounded ToString clause. The grid does not evaluate exclusions yet - IsUniformGrid already answers false so nothing downstream lies in the interim; the evaluator lands in the next slice. Spec: review/recurring-exclusions-spec.md.
The filtered door: GetNextOccurrence discards excluded candidates through an anchored loop that always advances from a real unfiltered occurrence, crossing each excluded region in one step where phase allows it - the uniform base grid jumps to the region exit through the self-verified tick arithmetic, cron asks Cronos from the exit (phase-free), calendar grids walk slot by slot. Day and date exclusions read the persisted zone's clock through the WallClock mapping (gap-removed midnights land on the gap exit); ranges compare absolute. Only the returned occurrence reports its own DST collapse count. A search that discards 200,000 candidates throws ExclusionSearchBudgetExceededException instead of answering null: an ended series is mathematics, an exhausted budget is not, and the callers route the difference (next slice). Skip-forward keeps its O(1) contract on fine grids by jumping the base grid and filter-fixing the candidate; IsUniformGrid and the constant-time miss count honestly answer false with exclusions present, and the no-exclusion path stays byte-identical. Spec: review/recurring-exclusions-spec.md.
The last slice: a stored cursor that a definition change left on an excluded slot is normalized through the evaluator's NormalizeCursorAsync (inclusive, never anchored on the possibly-off-grid stored value) before the inline recovery decision and the durable plan; the durable planner persists the normalized cursor through the cursor-only CAS even when the plan materializes nothing, inline recovery re-derives deterministically and never writes, and a pending first-run override is exempt by its provenance - SpecificRunTime only on cursor equality, RunNow/InitialDelay inherently selected at run zero. The search-budget exception now has one route per path: dispatch and every schedule-manager write surface it with nothing persisted, startup recovery sends it through the bounded-attempt counter to poison, the materializer re-parks, and the live advance records the completed run first - cursor retained, ScheduleRunAlreadyRecorded so a storage-less retry cannot count the run twice, and a lost versioned advance re-parks from the row that took over. RebaseFromCursor refuses exclusions on either side, naming RecalculateFromNow. ET0010 no longer reports a chain carrying Except/ExceptWeekends on either side of InTimeZone. User docs, integrate-evertask skill, CHANGELOG and README updated in the same slice per the anti-stale rule. Spec: review/recurring-exclusions-spec.md. Closes #36 (tranche 1; named calendars stay open as their own issue).
…#36) The exclusion-budget retry path is now a true twin of the provider retry. The storage-backed retry resumes the interrupted ADVANCE instead of re-entering the recovery decision, so a run already recorded on the row is never delivered twice; restart safety comes from a runtime marker (retained cursor + post-run count) persisted in the existing RuntimeInfo JSON in the same commit as the run - self-invalidating the moment the row really advances, no schema change. Built-in stores write it atomically through the new RecordRecurringRunForExclusionRetry storage member (a default composed of existing members keeps custom stores compiling, with their historical two-write crash window documented). The deferral and the failed park now publish Warning/Error monitoring events beside their log lines, and the refusal reports itself with its own line. The overloaded null is gone from the exclusion paths: FirstOccurrenceOnOrAfter exhausting its probe budget with exclusions present throws the typed budget exception instead of reading as "the grid has no occurrence" (a live series was silently finalized - reproduced RED first on a filtered weekly grid); a uniform-base jump that fails self-verification falls back to the anchored walk; the filter loop carries the same defensive no-progress bail as the file's other walks. The no-exclusions paths stay byte-identical. Coverage the review demanded: versioned and unversioned budget-catch variants (run counted exactly once, cursor retained, no double execution), inline-recovery cursor normalization (fires Monday, row keeps Saturday), cron region-jump cost (a 365-day window that a per-slot walk cannot afford), ET0010 forward-walk conversion unwrap (latent one-token defect) - all RED first where a defect existed. Review: review/recurring-exclusions-adversarial-review.md (1 P0, 2 P1, 4 P2 confirmed; 2 under-verified hardened defensively; 5 refuted).
…r the retry marker, skill bump (#36) The README gets a real showcase of the feature beside the other "closer look" sections, not just the Key Features bullet. custom-storage.md documents the new RecordRecurringRunForExclusionRetry member in the atomic-operations table (default two-commit composition vs atomic override). The CHANGELOG covers what the review fix round added: the restart-surviving retry marker, the two monitoring events and the storage member. The recurring guide names the backoff's monitoring events. integrate-evertask skill bumped to 2.1.0 for the exclusion sections it gained. The Recurring CLAUDE.md gotcha follows the fix-round reality (marker, resume semantics, FirstOccurrenceOnOrAfter typed cap failure).
…er 2 review rounds
…alidation (#36) First slice of tranche 2: AddScheduleCalendar collects named exclusion sets during the AddEverTask callback and the deep-frozen registry snapshot is created and registered as an instance the moment the callback returns - singleton factories are lazy, and a freeze at resolution time would let later mutations of the configuration object change this host's calendar meanings. ExceptCalendar joins the fluent surface with the usual DIM-plus-redeclaration pattern; ScheduleExclusions gains the persisted Calendars names (trimmed, sorted, deduplicated, at most 16), and a calendars-only exclusion set is no longer normalized away. The tranche-1 normalization moved into ScheduleExclusionNormalizer, shared verbatim by validation, registration and the registry's single resolution primitive: names are capped before any lookup, the flattened union is re-normalized after the merge, so the 1000-entry cap and the all-seven-days refusal now judge the union a schedule actually evaluates. Validation travels as ScheduleValidationContext (providers + calendars) at dispatch and schedule-manager ingress. Serialization pins three contracts: no-exclusion rows stay byte-identical, tranche-1-shaped JSON still reads, and a rewrite adopts the canonical shape with the empty Calendars member - legal only because the exclusion shape ships for the first time in 4.0.0. The grid does not evaluate calendars yet: the door still filters inline exclusions only. The evaluator's resolved clone and the row-rebuild validation land in the next slice. Spec: review/recurring-exclusion-calendars-spec.md.
The evaluator resolves the named calendars once per top-level call: the registry's primitive flattens inline plus calendar sets into a canonical union installed on a transient evaluation clone (Calendars emptied), and the existing pure math runs unchanged on it. A definition whose names were never resolved is refused by the filtered door - an evaluator built without the registry (the hand-wired fallback) refuses rather than silently running through every holiday - and the no-exclusions fast path stays byte-identical. The ScheduleValidationContext now travels every path that rebuilds a row into something that may park or execute: startup recovery, the schedule retry decision, the materializer and its repark builder, and ReparkFromRowAsync - a row naming a calendar this host does not register is poisoned with the calendar in the reason instead of executing once. The registry-free surfaces are pinned by tests: ToString reports names verbatim, TaskScheduleFacts and GetMinimumInterval never resolve, ScheduleRebase keeps refusing exclusions. Forward-only edit semantics pinned end to end: narrowing exposes only slots at or after the standing cursor, widening revokes nothing already materialized, a halt survives an edit, a live exclusion-retry marker resumes against the new union, and an edit that makes the resolved union invalid poisons at recovery. ET0010 stays silent on ExceptCalendar in both chain orders. User docs and the integration skill cover AddScheduleCalendar and ExceptCalendar. Spec: review/recurring-exclusion-calendars-spec.md.
…kill bump 2.2.0 (#36)
…ired on FromRow (#36) The calendars review confirmed one finding - a pinning gap, not a runtime defect: the ScheduleValidationContext reaching RecoveredTaskFactory.FromRow was an optional parameter, so a caller silently dropping it compiled and the suite stayed green while a row naming an unknown calendar would execute once through repark, schedule retry or the materializer walk. Three tests now hold those guards (each proven RED by removing the context from its production call site, then restored), and the contract is structural: production FromRow REQUIRES the context; the deliberately registry-free callers go through the explicit FromRowWithoutRegistries seam. Review: review/recurring-calendars-adversarial-review.md (1 P2 confirmed, 6 refuted, 0 under-verified).
GiampaoloGabba
had a problem deploying
to
release
August 31, 2026 14:51 — with
GitHub Actions
Failure
This branch had an error being deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
EverTask 4.0: durable occurrences, misfire policies, time zones, runtime schedule management, exclusions
The 4.0 epic: a recurring schedule is no longer frozen at dispatch, and a downtime no longer silently loses work.
What ships
Skip(default, unchanged),FireOnce,CatchUpwith mandatory caps (CatchUpOptions(maxAge, maxOccurrences), overflowHalt/SkipOldest) — a backlog is replayed observably, never invented and never unbounded.Context.ScheduledAtUtc/ScheduledAtLocal/Misfire(kind, missed range, exact-or-lower-bound count, lateness).InTimeZone, IANA persisted): 09:00 means 09:00 there, all year; gap and repeated-hour rules are pinned against Cronos as oracle.ITaskScheduleManager): reschedule/reevaluate/resume/cancel/requeue with compare-and-swap discipline;RebaseFromCursorfor phase-preserving cadence changes.INextOccurrenceProvider): the grid can come from your own calendar, with declared determinism, typed transient failures and bounded walks..Except(days/dates/windows),.ExceptWeekends(), and named host-level calendars (AddScheduleCalendar+.ExceptCalendar("it-holidays")) — an excluded slot never exists (no run, no misfire count, no row, no event); calendar edits apply forward-only from an immutable per-host snapshot.[LoggerMessage]logging, System.Text.Json serialization.Verification
review/).Compatibility
Closes #23
Closes #36
https://claude.ai/code/session_01Gh9X6ALEP5DSNSQYDdpBru