slotlock 0.2.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +582 -0
  2. package/CONTRIBUTING.md +35 -8
  3. package/GOVERNANCE.md +4 -2
  4. package/MAINTAINERS.md +3 -2
  5. package/NOTICE +4 -4
  6. package/README.md +498 -65
  7. package/SECURITY.md +31 -2
  8. package/SPEC.md +142 -15
  9. package/agent-server.d.ts +62 -10
  10. package/agent-server.js +577 -105
  11. package/agent-store-backend.d.ts +12 -0
  12. package/agent-store-backend.js +1115 -55
  13. package/api-keys.d.ts +11 -3
  14. package/api-keys.js +29 -6
  15. package/application-api.d.ts +35 -1
  16. package/application-api.js +683 -12
  17. package/booking-rules.d.ts +84 -0
  18. package/booking-rules.js +149 -0
  19. package/config.d.ts +112 -0
  20. package/config.js +225 -0
  21. package/dashboard.d.ts +67 -5
  22. package/dashboard.js +1012 -140
  23. package/ddl.d.ts +45 -3
  24. package/ddl.js +1124 -13
  25. package/engine.d.ts +5 -0
  26. package/engine.js +23 -6
  27. package/event-feed.d.ts +178 -0
  28. package/event-feed.js +435 -0
  29. package/event-schema.d.ts +59 -0
  30. package/event-schema.js +85 -0
  31. package/ical-worker-thread.js +4 -2
  32. package/ical-worker.d.ts +8 -5
  33. package/ical-worker.js +9 -2
  34. package/index.d.ts +12 -6
  35. package/index.js +9 -4
  36. package/interop.d.ts +117 -0
  37. package/interop.js +356 -0
  38. package/maintenance.d.ts +27 -5
  39. package/maintenance.js +92 -28
  40. package/mcp-app.d.ts +7 -1
  41. package/mcp-app.js +259 -40
  42. package/metrics.d.ts +24 -0
  43. package/metrics.js +167 -0
  44. package/node-server.d.ts +9 -1
  45. package/node-server.js +7 -2
  46. package/openapi.json +10970 -0
  47. package/package.json +3 -2
  48. package/rate-limit.d.ts +80 -0
  49. package/rate-limit.js +741 -0
  50. package/rest-api.d.ts +22 -0
  51. package/rest-api.js +166 -11
  52. package/rules.js +6 -3
  53. package/self-host.d.ts +45 -8
  54. package/self-host.js +616 -46
  55. package/slotlock-manifest.schema.json +7 -1
  56. package/slotlock.manifest.json +8 -2
  57. package/store.d.ts +647 -28
  58. package/store.js +2982 -327
  59. package/sync.d.ts +43 -2
  60. package/sync.js +1515 -323
  61. package/timezone.d.ts +58 -3
  62. package/timezone.js +249 -20
  63. package/types.d.ts +300 -9
  64. package/types.js +0 -2
  65. package/webhooks.d.ts +91 -0
  66. package/webhooks.js +300 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,588 @@
3
3
  All notable changes follow [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and semantic
4
4
  versioning.
5
5
 
6
+ ## 0.5.0 - 2026-10-28
7
+
8
+ ### Documentation
9
+
10
+ - The production checklist advises a `transaction_timeout` on the serving role (PostgreSQL 17+),
11
+ so a caller's transaction a store call runs inside cannot hold its locks and snapshot without
12
+ bound; on 16, `idle_in_transaction_session_timeout` and `statement_timeout`. A test pins the
13
+ setting on the CI's PostgreSQL 17 and 18.
14
+
15
+ ### Added
16
+
17
+ - How long history is kept is a setting: `SLOTLOCK_RETENTION_DAYS` (`retentionDays` in
18
+ `createSlotlockStore`), 1 to 3,650 days, 30 by default. It sets how long the change feed is kept
19
+ and how far back a cursor reaches, and ended waitlist entries; above 30 days, retired idempotency
20
+ answers and booking references too (never under 30). `slotlock maintain` reads it as `serve` does.
21
+ - A feed cursor answers `cursor_expired` exactly when a change after it has been deleted, by this
22
+ server's maintenance or any other (an older server's in a rolling deploy, an operator's): a trigger
23
+ on the feed records how far each tenant's was pruned (`slotlock.outbox_pruned`, created once and
24
+ started at each tenant's oldest kept change). How old a cursor is no longer matters, so a server
25
+ keeping changes longer than the one that pruned them never reads past a gap unseen, and a reader
26
+ that is up to date stays readable. Maintenance prunes the oldest changes in feed order, never past
27
+ one it keeps or a transaction still running, so a reader starting over always reads on.
28
+ - One holder's holds are bounded in time as well as number: `maxHeldHoursPerHolder` in
29
+ `createSlotlockStore` refuses a hold that would bring the holder's live holds past that many
30
+ hours together (`held_time_limit_reached`), and `maxHoldsPerHolder` sets the count that was fixed
31
+ at 20. `slotlock serve` sets them with `SLOTLOCK_LIMITS` (`holder_holds`, 20;
32
+ `holder_held_hours`, 2,160).
33
+ - `slotlock_get_pool_availability` (`GET /v1/pool-availability`): how many of a pool's members are
34
+ free across a window, the window split where that number changes, with coverage certainty, so an
35
+ agent can say "three vans free 9 to 11, one after" before booking with `slotlock_book_pool`.
36
+ - Prometheus metrics: with `SLOTLOCK_METRICS_TOKEN`, `slotlock serve` answers `/metrics` to that
37
+ bearer token: calls by protocol, operation and outcome with their duration, confirmations,
38
+ subscriptions, refusals, maintenance and webhook deliveries, labelled by closed sets only (never a
39
+ tenant, key or client). `createSlotlockMetrics` counts the same from a server's own hooks.
40
+ - The booking rule `requireCertain` (`require_certain`): a booking must lie in time every calendar
41
+ sync recording coverage for the resource has read within a day, or it is refused (`uncertain`,
42
+ `booking_rule_uncertain` to an agent), rather than booked over what a stale or failing sync may
43
+ hide. Searches on such a resource, the store's and the agents', require the same coverage, of the
44
+ slot and its turnaround, within the same day. The dashboard's rules form has it.
45
+ - `DELETE /v1/resources/{id}/coverage/{source}` (`slotlock_delete_coverage`, `deleteCoverage`)
46
+ retires a calendar sync: free/busy and `requireCertain` stop waiting on its coverage.
47
+
48
+ - The dashboard is an app with a sidebar: an Overview with the coming week's bookings, events and
49
+ live holds across every resource and the agents' latest changes, named by key; Resources; API
50
+ keys, with the endpoints; and Settings. `SLOTLOCK_DASHBOARD_NAME` (`name` in
51
+ `createSlotlockDashboard`) names the deployment. Works without JavaScript, light and dark.
52
+ - Dashboard pages for 0.4's features: a page per resource (bookable hours, booking rules, pool, its
53
+ coming bookings and activity), Waitlist (entries and their offers; removing one offers its time
54
+ on) and Webhooks (add with the secret shown once, delete, re-enable, and each endpoint's health:
55
+ delivering, failing since when and why, or disabled). `SlotlockDashboardResources` takes them as
56
+ optional `setRules`, `setPool`, `waitlist` and `webhooks`; a page shows only when its adapter
57
+ offers it.
58
+ - `listAgenda` reads what occupies a tenant's resources in a window, every resource at once:
59
+ bookings, calendar events' occurrences and live holds. `listRecentChanges` reads a tenant's
60
+ latest changes, newest first. `SlotlockDashboardResources` takes both as optional `agenda` and
61
+ `recentChanges`.
62
+
63
+ - Organizations in the dashboard: with `SLOTLOCK_DASHBOARD_ORGS` (GitHub organization ids), a
64
+ tenant is an organization, `github-org:<id>`, that its members share. Sign-in asks for `read:org`
65
+ and reads the person's active memberships as an owner or member (not a billing manager); a member
66
+ of several served organizations switches between them, and someone in none is refused. A form
67
+ works only for the organization it was opened in, and the key list says who made each key.
68
+ Removing an organization from the list signs its sessions out and stops its keys in `slotlock
69
+ serve`, which reads the list whether or not it serves the dashboard itself. `createSlotlockDashboard` takes
70
+ `organizations`.
71
+
72
+ ### Changed
73
+
74
+ - A key created in the dashboard records the person who created it (`github:<user id>`) as
75
+ `createdBy`, not the tenant; for a personal tenant the two are the same.
76
+
77
+ ### Fixed
78
+
79
+ - Time an agent frees is offered to the waitlist at once, as the application API's cancels already
80
+ were: an agent releasing its holds (`slotlock_release_slots`), deleting or moving an event, and a
81
+ booking moved through `PUT /v1/reservations`. Before, it waited for the next maintenance pass, up
82
+ to an hour.
83
+
84
+ ## 0.4.1 - 2026-10-28
85
+
86
+ ### Fixed
87
+
88
+ - The Python client is published to PyPI again. 0.4.0's PyPI job could not build it: hatchling, the
89
+ build backend, now needs `tomlkit`, which the release's pinned build requirements did not list.
90
+ 0.4.1 is otherwise 0.4.0; the other packages are released at 0.4.1 with no change.
91
+
92
+ ## 0.4.0 - 2026-10-28
93
+
94
+ ### Added
95
+
96
+ - Waitlists: `POST /v1/waitlist` (`slotlock_join_waitlist`, `joinWaitlist`) waits for a window on a
97
+ resource or in a pool; `GET /v1/waitlist` and `DELETE /v1/waitlist/{entry_id}` list and leave it.
98
+ Freed time is offered in the order entries joined, as a hold for the waiter (`waitlist.offered` on
99
+ the feed): at once after a cancel or release through the API, and by maintenance otherwise, which
100
+ also expires entries whose window started (`waitlist.expired`).
101
+ - Check-in and no-show release: the booking rule `checkInMinutes` (`check_in_minutes`) releases a
102
+ booking nobody claimed that long after its start (`reservation.no_show`), for bookings made once
103
+ the rule is set; `POST /v1/reservations/{reservation_ref}/check-in` (`slotlock_check_in_reservation`,
104
+ `checkInReservation`) claims one from an hour before its start (`reservation.checked_in`); a
105
+ move to another start clears it, and a walk-in's window runs from when it was booked. Maintenance results gain `noShows`
106
+ and `waitlistOffers` (`no_shows`, `waitlist_offers`). Run `slotlock migrate`.
107
+ - Calendar interchange: `GET`/`PUT /v1/resources/{resource_id}/availability/ical`
108
+ (`slotlock_export_resource_availability`, `slotlock_import_resource_availability`) write and read
109
+ a resource's weekly bookable hours as an iCalendar VAVAILABILITY (RFC 7953); an import must be in
110
+ the resource's zone or floating, and anything weekly hours cannot say answers 400
111
+ `invalid_availability`. `GET /v1/tenant/events/{event_id}/jscalendar`
112
+ (`slotlock_export_tenant_event`) answers an event as a JSCalendar Event (RFC 8984). The library
113
+ exports `emitVAvailability`, `parseVAvailability` and `toJSCalendarEvent`.
114
+ - `npm run bench` (`scripts/bench.mjs`, `bench/*.sql`): RFC 0002's pgbench workloads, Slotlock's
115
+ range constraint against one row per 15-minute slot, reproducible on any scratch database. A new
116
+ Scaling page covers partitioning by resource (PostgreSQL 16, 17+), Citus, a home region per tenant,
117
+ read replicas, tracing and SLOs, backups and point-in-time recovery, and committing a solver's
118
+ plan through Slotlock.
119
+ - Shared request budgets: `SLOTLOCK_SHARED_LIMITS=on` makes every server of a deployment draw a
120
+ tenant's `tenant_*` and `app_*` budgets from one token bucket in PostgreSQL
121
+ (`slotlock.take_rate_token`, keyed by a digest), after its own buckets pass; a database that
122
+ cannot be reached leaves each server's own budgets in charge. `createSlotlockRequestLimits` takes
123
+ `shared`. Run `slotlock migrate`.
124
+ - Feed health: `GET /v1/resources/{resource_id}/coverage` (`slotlock_get_coverage`, `listCoverage`)
125
+ shows each source's window, last read, last error and whether it is stale;
126
+ `POST /v1/resources/{resource_id}/coverage/{source}/failure` (`slotlock_record_coverage_failure`,
127
+ `recordCoverageFailure`) records a failed read. Maintenance puts a source that has gone stale on the
128
+ feed once (`coverage.stale`, `reportStaleCoverage`) and counts `staleSources` (`stale_sources`).
129
+ Unread time stays unproven, never free. Run `slotlock migrate`.
130
+ - Time zone rule changes: each calendar event keeps the wall time it was booked for in its zone,
131
+ and maintenance reports a future one-off event whose wall time now falls at another instant
132
+ (`event.timezone_shifted` on the feed, once), or with `followZoneRules` moves it there as a new
133
+ revision unless that overlaps (`event.timezone_conflict`). `store.followZoneRules` runs it alone.
134
+ Maintenance results gain `zoneShifts` (`zone_shifts` in the API); `/healthz` gains `tzdata`. Run
135
+ `slotlock migrate`.
136
+ - History: every change to a booking, hold or event records who made it (the API key or agent:
137
+ `withTenant`'s new `actor`, which the agent and application backends set to the principal's
138
+ subject) and, for a change of an existing record, what it was before. Read it newest first with
139
+ `GET /v1/resources/{resource_id}/history`, `GET /v1/reservations/{reservation_ref}/history` and
140
+ `GET /v1/tenant/events/{event_id}/history`, or `listHistory`. A backend of your own implements
141
+ `getHistory`. The feed and history now keep changes 30 days. Run `slotlock migrate`.
142
+ - Pools: interchangeable resources sharing a class (a vehicle class, a room type).
143
+ `PUT /v1/resources/{resource_id}/pool` (`slotlock_set_resource_pool`, `setResourcePool`) puts a
144
+ resource in one; `GET /v1/pools` lists them with their capacity. `slotlock_book_pool` (REST
145
+ `POST /v1/pool-bookings`) books the first member free and allowed in one transaction, so a class
146
+ sells out exactly at its size however many race (`pool_full`), and replays to the member it
147
+ booked. `slotlock_list_resources` shows each resource's `pool` and takes `pool` to list one's
148
+ members. Resources in the application API have `pool`. A backend of your own implements
149
+ `setResourcePool` and `listPools`, and the optional `bookPool`. Run `slotlock migrate`.
150
+ - Bookings across resources: `slotlock_book_together` (REST `POST /v1/bundles`) books the same
151
+ time on 2-20 resources in one transaction, all or none, and its refusal names the `resource_id`
152
+ that refused; bundles lock in one order, so racing ones never deadlock. It is a write that
153
+ `SLOTLOCK_CONFIRM_WRITES` can guard. `slotlock_find_available_slots` takes `require_free` for
154
+ times when every resource, or any n, is free together, each slot naming them in `resource_ids`.
155
+ A backend of your own implements the optional `bookTogether`, else `bundles_unavailable`.
156
+ - `SlotlockAgentOperationError` takes `details` (at most five short fields, such as
157
+ `resource_id`), which every protocol carries beside `code`.
158
+ - An event feed and webhooks. Every committed change to a tenant's holds, bookings and calendar
159
+ events is recorded in the same transaction by trigger: ids, times and states, never titles or
160
+ attendees. `GET /v1/changes` (`slotlock_list_changes`, `listChanges`) lists them as CloudEvents
161
+ by a cursor that no late-committing transaction can be skipped behind; changes are kept 30 days.
162
+ `POST /v1/webhooks` (`slotlock_create_webhook` and list, delete, enable) sends them in order,
163
+ signed as Standard Webhooks, at least once per `webhook-id`, retried with backoff for a day and
164
+ then disabled (`webhook.disabled` on the feed). `slotlock serve` delivers (`SLOTLOCK_WEBHOOKS`),
165
+ never to a private network address. Maintenance prunes the feed. A backend of your own
166
+ implements the five new `SlotlockApplicationBackend` methods. Run `slotlock migrate`.
167
+ - Booking rules per resource (`SlotlockBookingRules`): minimum notice, how far ahead, the step
168
+ starts fall on in the resource's time zone, a turnaround kept free after each booking (as its
169
+ trailing buffer, which the exclusion constraint enforces), and whether bookings must lie inside
170
+ its hours. Holds, reservations, moves and agents' events are refused with the rule they break
171
+ (`booking_rule_violated`; to an agent `booking_rule_min_notice`, `_max_advance`, `_slot_step`,
172
+ `_outside_hours`), by the database's clock; searches offer only what the rules allow. Set them
173
+ with `slotlock rules set`, `PUT /v1/resources/{resource_id}/booking-rules`
174
+ (`slotlock_set_resource_booking_rules`) or `setResourceBookingRules`. `putCalendarEvent` checks
175
+ them with `enforceBookingRules`; an imported event is not checked. `createSlotlockStore` takes
176
+ `defaultAvailabilityRules` for resources without hours of their own (`slotlock serve` passes
177
+ `SLOTLOCK_AVAILABILITY`). Run `slotlock migrate`.
178
+ - Agent holds: `slotlock_hold_slots` (REST `POST /v1/slot-holds`) holds up to five candidate slots
179
+ for 30-900 seconds as one group while a person chooses, listing the taken ones by index;
180
+ `slotlock_create_event`'s `hold_id` books one, releasing the others in the same transaction; and
181
+ `slotlock_release_slots` (`DELETE /v1/slot-holds/{hold_id}`) releases a group at once. A hold is
182
+ its agent's alone, and one agent may have 20 live (`SLOTLOCK_MAX_HOLDS_PER_HOLDER`). In the
183
+ library, `acquireHold` takes `holderRef` and `groupId`, `releaseHold` a `holderRef`, and
184
+ `putCalendarEvent` `fromHold` (refused with `hold_not_found` or `hold_mismatch`). A backend of
185
+ your own implements the optional `holdSlots` and `releaseSlots`; without them the tools answer
186
+ `holds_unavailable`. Run `slotlock migrate`. In an MCP Apps host the calendar view renders the
187
+ held slots, with when they expire, and its message names the hold, so the person picks from time
188
+ nobody can take while they decide.
189
+ - `slotlock_find_available_slots` (REST `GET /v1/available-slots`) lists up to 50 certain slots of
190
+ a duration, earliest first across up to 20 resources, each found the way
191
+ `slotlock_find_next_available` finds one: free of busy time, inside the resource's hours, and
192
+ only when every resource's coverage is certain.
193
+ - The MCP Apps calendar view lets a person pick one of those slots: a strip of days, each day's times
194
+ by part of the day, and *Book this time*. It sends the agent a chat message naming the slot
195
+ (`ui/message`), so the agent books it through the normal, confirmable path, and shows the text to
196
+ send when the host refuses.
197
+ - The view follows the host: its theme and its standard style variables, which it now applies (it
198
+ only read them before, so it looked the same, and light, in every host). It shows times in the
199
+ person's locale, clock and time zone, and reports its size to the host.
200
+
201
+ ### Changed
202
+
203
+ - REST refusals are RFC 9457 problem details (`application/problem+json`): `type` (a tag URI, one per
204
+ code), `title`, `status` and `code`, with `error.code` kept so existing clients read them as
205
+ before, and a `Link: rel="describedby"` header to the error docs. An `overlap` names the
206
+ `resource_id`, `start` and `end` it refused.
207
+ - The same idempotency key with another request answers REST `422 idempotency_conflict` (was 409),
208
+ as draft-ietf-httpapi-idempotency-key-header-08 says. MCP and A2A answer the same code.
209
+ - Every REST write that keeps an `idempotency_key` (the agent tools' as well as the application's)
210
+ takes it as the `Idempotency-Key` header, as a Structured Field String (`"…"`) or bare; a write
211
+ that requires one and gets neither answers `400 idempotency_key_required`. Keys are kept 30 days
212
+ for event writes and while the hold could be live for holds; the docs publish the policy.
213
+
214
+ - The application API's resource has `booking_rules` (`null` when it has none) and `pool` (`null`
215
+ when it is in none).
216
+ - `SlotlockApplicationBackend` has twenty more methods, one per new application operation:
217
+ `setResourceBookingRules`, `setResourcePool`, `listPools`, `getResourceHistory`,
218
+ `getReservationHistory`, `getTenantEventHistory`, `getCoverage`, `recordCoverageFailure`,
219
+ `listChanges`, `createWebhook`, `listWebhooks`, `deleteWebhook`, `enableWebhook`,
220
+ `exportResourceAvailability`, `importResourceAvailability`, `exportTenantEvent`,
221
+ `checkInReservation`, `joinWaitlist`, `listWaitlist` and `leaveWaitlist`. A backend of your own
222
+ implements them, or wraps `createSlotlockStoreApplicationBackend`, which has them all. The agent
223
+ backend's new methods (`holdSlots`, `releaseSlots`, `bookTogether`, `bookPool`) are optional: on
224
+ a backend without one, its tool answers `409` with `holds_unavailable`, `bundles_unavailable` or
225
+ `pools_unavailable`.
226
+ - Maintenance results (`SlotlockMaintenanceResult`, and `POST /v1/maintenance`) have more counts:
227
+ `zoneShifts`, `staleSources`, `noShows` and `waitlistOffers` (`zone_shifts`, `stale_sources`,
228
+ `no_shows`, `waitlist_offers`).
229
+ - The three write tools carry `_meta.ui.visibility: ["model"]`: MCP Apps hosts no longer let a view
230
+ call them (the default is model and app).
231
+
232
+ ### Fixed
233
+
234
+ - Retrying a recurring event's create (or any put) with the same idempotency key and arguments
235
+ after UTC midnight replays the first result again. The command's hash included the window its
236
+ occurrences are stored over, which for a series is the rolling horizon starting at the current
237
+ UTC midnight, so the next day's retry was refused as `idempotency_conflict`. A command stored by
238
+ 0.3.0 still replays: a one-off event's always, a series' on the day it was stored.
239
+ - A hold's expiry is judged at each statement (`statement_timestamp()`), not at the start of the
240
+ transaction (`now()`). A store made from a caller's transaction runs in savepoints of it, so a hold
241
+ that expired while that transaction ran still looked live there and could be confirmed after its
242
+ expiry. Its time to live also starts when it is asked for, not when the caller's transaction did.
243
+ - An accepted write confirmation runs its write once. `slotlock serve` records each acceptance's
244
+ nonce in Postgres until it expires, on every server sharing the database, and refuses the same
245
+ acceptance sent again (`confirmation_invalid`); before, it could be sent again until it expired
246
+ (the write's idempotency key or revision then decided). `SlotlockAgentConfirmationOptions` takes
247
+ `consumeAcceptance` for this, and `createSlotlockAcceptanceRecords(sql)` implements it. Run
248
+ `slotlock migrate`: `serve` refuses to start with `SLOTLOCK_CONFIRM_WRITES` on a schema that cannot
249
+ record them.
250
+ - The production checklist's erasure item names `slotlock tenant erase` and `eraseTenant`, which
251
+ delete everything a tenant owns in one transaction, instead of a table-by-table delete.
252
+
253
+ - The OpenAPI document declares the `404` and `409` the agent-tool routes answer: `404` on
254
+ free/busy, next available and reading or listing events, and `404` and `409` (`overlap`, a stale
255
+ revision) on creating, changing and cancelling one. Clients generated from it now type an overlap
256
+ instead of falling to `default`.
257
+
258
+ ## 0.3.0 - 2026-10-27
259
+
260
+ 0.3.0 fixes the double bookings 0.2.0 could accept around daylight-saving changes and makes
261
+ `slotlock serve` safe to share between tenants. Its themes: a recurrence engine that keeps every
262
+ occurrence's whole time across clock changes; per-tenant limits, suspension and erasure in the
263
+ store; request limits and fair shares of the server in `slotlock serve`; the application API's
264
+ reads, resource lifecycle and tenant calls; a release path that checks what it publishes and
265
+ publishes the five language clients; and fixes for the findings of a security audit. Read the
266
+ upgrade notes first.
267
+
268
+ ### Upgrade notes
269
+
270
+ - Run `slotlock migrate` with 0.3.0 before serving it. The schema now records its version, and
271
+ `slotlock serve` refuses a database an older release migrated (`schema_outdated`) or a newer one
272
+ did (`schema_newer`). A migration refuses a schema a newer release migrated, and PostgreSQL older
273
+ than 16. It also grants the serving role what this release needs: a role without that grant is
274
+ refused `application_role_not_granted`, so a library user runs `grantApplicationRole` again.
275
+ Each migration step waits at most a second for a lock and tries again, so serving queries queue
276
+ behind it for at most that long, and an unchanged schema takes no table lock.
277
+ - Before upgrading, run `storedCalendarEventProblem(event)` over your stored events. A series an
278
+ earlier release stored and this one refuses (a rule shape it no longer expands, `COUNT` with a
279
+ yearly day some years lack, or an exception that names no occurrence) keeps its old coverage and
280
+ is counted as refused at every roll.
281
+ - Breaking: `slotlock serve` defaults `SLOTLOCK_CONFIRM_WRITES` to `none`, so an authenticated
282
+ write takes effect at once. Set `all`, or a list of write tools, to keep confirmation;
283
+ `SLOTLOCK_CONFIRMATION_SECRET` is required only then.
284
+ - Breaking behind a proxy: `slotlock serve` now limits requests per client address, so set
285
+ `SLOTLOCK_TRUSTED_PROXY_HEADER` (`CF-Connecting-IP` behind Cloudflare, also when kamal-proxy or
286
+ another proxy sits between) before upgrading a server behind one. Without it every client counts
287
+ as the proxy's one address and shares one `address` budget, 50 requests a second by default.
288
+ - `slotlock serve` applies new limits by default, each set with `SLOTLOCK_LIMITS`: per tenant,
289
+ 10,000 holds (`tenant_holds`) and 100,000 each of bookings, events and coverage rows
290
+ (`tenant_reservations`, `tenant_events`, `tenant_coverage`); 256 live-update streams on the
291
+ server (`subscriptions`), 16 a tenant (`tenant_subscriptions`) and 4 a key
292
+ (`key_subscriptions`); 2 of the pool's 10 connections a tenant (`tenant_transactions`; a server
293
+ with one tenant can set 10); one recurring write at a time per tenant and per server
294
+ (`tenant_series_writes`, `series_writes`) and one application maintenance run
295
+ (`maintenance_runs`); and request rates (Added). Check them against your largest tenant.
296
+ - Clients should handle the new answers: 429 `rate_limited` with `Retry-After`; 503
297
+ `server_busy` (a full key-lookup or series-write queue, or a transaction that waited more than 5
298
+ seconds for a lock) and 503 `request_aborted` (the request ended before its call did); 400
299
+ `calendar_work_limit` from free/busy, next-available and busy reads; 409
300
+ `hold_limit_reached`, `reservation_limit_reached`, `event_limit_reached`,
301
+ `coverage_limit_reached` and `resource_limit_reached`; and 403 `tenant_suspended`.
302
+ - Writes 0.2.0 accepted that are now refused: a control character other than tab or line break in
303
+ an event's title, description, location or participant names, or any in its UID
304
+ (`invalid_event`); event times outside years 1 to 9999; rule shapes ical.js expands differently
305
+ from RFC 5545 (`recurrenceRuleProblem(rule)` says why), and in a stored rule `COUNT=0`, an
306
+ unknown or repeated part or a sub-daily frequency; DTEND with DURATION; an exception that names
307
+ no occurrence; more than 8 RRULEs on a feed event; in the trusted importer, a DTEND the clocks
308
+ skip, a DURATION in days or weeks outside UTC and an all-day series whose first midnight is
309
+ skipped; and a booking, hold or event past the rolling horizon on a resource whose series goes on
310
+ past it (`recurrence_not_materialized`).
311
+ - Breaking for library callers: `findNextAvailableFor` requires `tenantRef` (`null` for a resource
312
+ with no tenant). `withTenant` opens its own transactions READ COMMITTED, and inside a caller's
313
+ REPEATABLE READ or SERIALIZABLE transaction refuses writes with `invalid_transaction_isolation`.
314
+ `createSlotlockApiKeyAuthenticator` throws `SlotlockAgentOperationError` `server_busy` (503) when
315
+ its lookup queue is full, instead of answering anonymous; `createSlotlockAgentServer` answers it
316
+ 503 with `Retry-After` on REST, MCP and A2A. During maintenance a series whose roll fails is
317
+ reported in `refusedEventIds` and put back for an hour instead of failing the pass.
318
+ - Clients and OpenAPI: new optional arguments come after the old ones in every client, and the
319
+ .NET client keeps 0.2.0's parameter lists as overloads, so a call passing the cancellation token
320
+ by position still compiles. A Rust input struct written out field by field needs
321
+ `..Default::default()` for the fields it gained (`CreateResourceInput`, `AcquireHoldInput`,
322
+ `UpdateEventInput`), and a Go one written without field names needs them. Event participant
323
+ addresses in responses are no longer `format: email` in openapi.json; they follow the store's
324
+ own grammar (one `@`, no spaces), so a client that checked them as email addresses should stop.
325
+ - The dashboard Worker takes a new binding, `SLOTLOCK_SIGN_IN_CEILING_RATE_LIMIT`, for every
326
+ client together. Until it is bound, `SLOTLOCK_SIGN_IN_RATE_LIMIT` counts every client together,
327
+ as before, and the Worker logs a warning: add the ceiling before lowering that binding's limit
328
+ to one client's.
329
+ - The server image runs Node.js 24 LTS (24.22.1). 0.2.0's image ran Node.js 26.11.1.
330
+
331
+ ### Security
332
+
333
+ - zod moves from 4.6.5 to 4.6.7 (#59), past GHSA-mrhx-c7xc-m3w5 (high): when validating an
334
+ oversized array, zod 4.6.5 collects an issue for every failing element with no cap, so one
335
+ authenticated tool or REST call could hold far more memory than its body. 0.1.0, 0.1.1 and 0.2.0
336
+ pin zod 4.6.5 exactly; their users can add `"overrides": {"zod": "4.6.7"}` to their own
337
+ `package.json` (README, Known issues in 0.2.0).
338
+ - `slotlock serve` had no request limits. It now limits requests per client address, key, tenant
339
+ and server (Added), and caps the work one tenant can start, store or hold open (Upgrade notes).
340
+ - A flood of made-up API keys no longer makes real keys answer 401 as if revoked. A full lookup
341
+ queue answers 503 `server_busy`, and keys that authenticated in the last 15 minutes wait in a
342
+ queue of their own, served first.
343
+ - The store's next-available search took no tenant and read no coverage: it offered slots on
344
+ another tenant's resource, on unknown ids and in a series' unstored time. It now answers
345
+ `resource_not_found` for those and offers a slot only where free/busy proves the time complete.
346
+ `acquireHold`, `createReservation` and `listBusy` take `tenantRef` and enforce it whatever the
347
+ connection's row-level security. An id the store never issues answers not found, not a raw error.
348
+ - One tenant can no longer hold every database connection of `slotlock serve`. 12 one-off creates
349
+ waiting on their tenant's lock held the shared pool of 10, and another tenant waited about a
350
+ second. A tenant's transactions now run at most 2 at once (`tenant_transactions`), and one that
351
+ waits more than 5 seconds for a lock answers 503 `server_busy`. `createSlotlockStore` takes
352
+ `maxTenantTransactions` and `tenantLockTimeoutMs` to do the same.
353
+ - `slotlock serve` bounds what one tenant may store. It gave its store no tenant limits, so an admin
354
+ key could add holds, bookings and coverage rows (and an agent events) without end. Every surface
355
+ now shares the store's tenant limits (Upgrade notes); a write past one answers 409 with its
356
+ `*_limit_reached` code.
357
+ - Expired holds now count against the hold limit until deleted, and keyed holds until they would
358
+ have expired, so one-millisecond holds cannot store rows without end between maintenance passes.
359
+ - Many tenants can no longer hold every live-update stream between them: 16 tenants within their
360
+ share could take all 256. When the server is full, a tenant holding fewer than the tenant holding
361
+ most, less one, ends that tenant's oldest stream gracefully (reported as `preempted`).
362
+ - One API key can no longer hold every live-update stream of its tenant: a key's cap
363
+ (`key_subscriptions`) is kept below its tenant's unless the tenant may hold only one.
364
+ - A key without the admin scope no longer spends its tenant's application API budget. Its refused
365
+ calls to application routes took tokens from `app_read` and `app_write`, so a read-only key could
366
+ keep the tenant's admin application answered 429.
367
+ - A burst of one tenant's recurring writes no longer holds the server from other tenants: such
368
+ writes wait for slots shared between tenants (Added). In the audit's test another tenant's
369
+ worst read went from 12.4 s to about 0.3 s.
370
+ - `slotlock_get_free_busy` and `slotlock_find_next_available` read a resource named more than once
371
+ once, and read 10,000 busy intervals in all across a request, answering `400
372
+ calendar_work_limit` past that. A request naming 100 resources, or one resource 100 times, did
373
+ 100 times the work of one.
374
+ - `slotlock_find_next_available` expands 100,000 bookable windows in all, answering `400
375
+ calendar_work_limit` past that, and walks a resource's busy time once rather than once per
376
+ window. 100 resources with 50 daily rules each, searched over a year, held the process for about
377
+ seven seconds.
378
+ - `slotlock_list_events` reads a resource named more than once once. One id named 100 times read
379
+ its events 100 times and returned the first event 100 times.
380
+ - The application API's busy read (`GET /v1/resources/{id}/busy`) stops at the first interval past
381
+ the 10,000 it may return, instead of reading the whole window first. `store.listBusy` takes a
382
+ `limit`.
383
+ - The recurrence budget counts work, not time, and now charges what each step costs, time zone
384
+ reads included. Fifty writes of `FREQ=DAILY;INTERVAL=9999;BYMONTH=2;BYMONTHDAY=29` held the
385
+ server for 23 seconds, and forty daily London series held a feed for 35 seconds; a refused write
386
+ now ends within about a tenth of a second, and a feed within about half a second.
387
+ - A feed event may list 8 RRULEs, and each one past the first costs a step at every occurrence it
388
+ passes. One event with 110,000 RRULEs (2 MB) held `parseICalendarFeed` for about 9 seconds.
389
+ - One recurring series can no longer stop its tenant's maintenance. A rule naming two wall times in
390
+ a spring-forward gap (`FREQ=DAILY;BYHOUR=2,3;BYMINUTE=0,30` in New York) gave one instant twice,
391
+ its roll failed every pass, and no other series of the tenant extended or had its expired holds
392
+ purged. An instant is now one occurrence wherever the rule names it again, and a series whose
393
+ roll fails for any other reason is refused and put back (Upgrade notes).
394
+ - An event's title, description, location and participant names may hold a tab or a line break,
395
+ and no other control character; its UID none. ical.js left a bare CR in what `emitICalendar` and
396
+ `emitITipCalendar` wrote, so a reader that ends lines at one read the rest as properties of its
397
+ own (`METHOD:CANCEL`). A CRLF line break is kept as LF.
398
+ - The Node listener refuses a request target with a dot segment between backslashes
399
+ (`/v1/resources\..\availability`), as it refuses one between slashes: URL parsing reads a
400
+ backslash as a slash. An encoded backslash (`%5C`) is still an ordinary character.
401
+ - A write confirmation no longer states a series' dates in UTC when it could not read the event (a
402
+ write-only key, a refused or slow read) and the update names no time zone. The store books the
403
+ series in the event's own zone, so the dates approved could differ from those booked. The
404
+ question now shows the rule and says its dates follow the event's existing zone.
405
+ - An event whose organizer or attendee address the store keeps but `z.email()` refuses, such as
406
+ `ops@localhost` from an imported feed, no longer makes every listing of its window answer 500 and
407
+ stop paginating (Upgrade notes).
408
+ - Dashboard sign-ins are counted per client network, in the Worker and in `slotlock serve`: an
409
+ IPv4 address, or an IPv6 client's /48, so a site rotating through its addresses cannot use up the
410
+ ceiling every other client shares. Every other request counts an IPv6 client as its /64. The
411
+ dashboard runs at most two sign-ins from one client at once.
412
+ - The example dashboard Worker configuration removes query strings, which hold OAuth codes, from
413
+ the URLs Workers Logs keep.
414
+ - SECURITY.md and the README say write confirmation is attested by the client: it keeps an agent
415
+ from writing unasked and is not an authorization boundary. SECURITY.md also says how hosted
416
+ Slotlock may be tested: your own account only, no other account's data, no load or denial of
417
+ service, reports through private vulnerability reporting.
418
+ - Development lockfiles only (nothing reaches the package or the image): the site's and the
419
+ dashboard Worker's Wrangler move undici from 7.29.1 to 7.30.1 (GHSA-xjj6-f33q-929c, high), and
420
+ the development lockfile moves body-parser from 2.3.0 to 2.4.0 (GHSA-27r3-jxmh-p828).
421
+
422
+ ### Added
423
+
424
+ - The application API serves the store calls it lacked, each in `openapi.json` and the five
425
+ clients: a resource's `visibility` (`private` ones are never shown to agents) and `archived_at`;
426
+ `POST /v1/resources/{id}/archive` and `…/restore`, and `DELETE /v1/resources/{id}` (`409
427
+ resource_in_use` while anything on it is live); `GET /v1/holds/{id}`,
428
+ `GET /v1/resources/{id}/holds`, `GET /v1/reservations/{ref}`,
429
+ `GET /v1/resources/{id}/reservations` and `GET /v1/resources/{id}/busy`, paged and bounded to
430
+ 367 days; `GET /v1/tenant/events` and `GET`/`DELETE /v1/tenant/events/{id}`, which reach and
431
+ cancel any key's event, a revoked one's included; and `GET /v1/tenant`, the key's tenant and
432
+ whether it is suspended.
433
+ - `POST /v1/tenant/erase` erases the key's tenant, with its reference as `confirm_tenant_ref`. It is
434
+ off unless the application backend is created with `tenantErasure: true` (`slotlock serve` leaves
435
+ it off) and refused while the tenant is suspended.
436
+ - `POST /v1/holds` and `DELETE /v1/tenant/events/{id}` take a retry identity as the
437
+ `Idempotency-Key` header as well as `idempotency_key`; a hold retried with it answers the first
438
+ hold, and the key reused with another request is `409 idempotency_conflict`. Another application
439
+ route refuses the header (`400 idempotency_key_not_supported`) rather than ignore it.
440
+ - `POST /v1/holds` takes `buffer_after_minutes` (0 to 30 days), as a booking does: the time after
441
+ the hold stays busy, and the booking it is confirmed as keeps it.
442
+ - In the store: private resources, which agents neither list, describe nor book; `archiveResource`,
443
+ `restoreResource` and `deleteResource`; holds with a turnaround buffer the booking keeps and a
444
+ retry identity; `getHold`, `listHolds`, `getReservation` and `listReservations`; and reading,
445
+ listing and cancelling any key's event as the tenant.
446
+ - Tenant limits in the store (`tenantLimits`: resources, holds, reservations, events, coverage),
447
+ refused with `*_limit_reached`. They count what is live or still to come plus what the tenant
448
+ added in the last 30 days, so ended history leaves them. Maintenance now deletes expired holds.
449
+ - Tenant suspension and erasure: the deployment role may `suspendTenant` (with a reason) and
450
+ `unsuspendTenant`; a suspended tenant may read, its writes are refused `tenant_suspended`, and it
451
+ gets no new API key. `eraseTenant` deletes every row a tenant owns in one call and says how many
452
+ from each table. No serving role may suspend.
453
+ - `slotlock resource archive|restore|delete <ref>` and `slotlock tenant status|suspend|unsuspend|erase`.
454
+ `suspend` and `unsuspend` connect as `SLOTLOCK_MIGRATE_DATABASE_URL`'s role, the only one that
455
+ may; `erase` needs `--confirm` repeating `SLOTLOCK_TENANT`.
456
+ - `slotlock_update_event` takes `expected_resource_id`: an event since moved to another resource is
457
+ not found, checked under the event's lock.
458
+ - `slotlock serve` limits requests: per client address before any key is looked up, then per API
459
+ key, per tenant and for the whole server, with reads and writes counted apart (by default 20
460
+ reads and 5 writes a second per key and per tenant). The application API counts in its tenant's
461
+ own budget (`app_read`, `app_write`). A refusal answers 429 `rate_limited` with `Retry-After` and
462
+ the `RateLimit` and `RateLimit-Policy` fields. `SLOTLOCK_LIMITS` changes the limits. Behind a
463
+ proxy without `SLOTLOCK_TRUSTED_PROXY_HEADER`, the first request carrying a client-address field
464
+ logs a `client_address_field_ignored` warning. The health check is not counted, and reuses one
465
+ database check a second.
466
+ - `slotlock serve` runs a recurring create and every event update one at a time per tenant and per
467
+ server (`tenant_series_writes`, `series_writes`), taking the write's token before it waits; each
468
+ server process counts its own slots. A freed slot rests for as long as the server was busy while
469
+ the write before held it, up to a second, so writes that each hold it under a second keep the
470
+ server busy at most about half the time. It then goes to the waiting tenant with the fewest
471
+ writes in flight and, among those, the one whose writes have kept the server busy least lately.
472
+ Tenants with more writes waiting never go ahead of one sending a write at a time, but many
473
+ tenants with one write each and less use, new ones included, can each go ahead of it once. Four
474
+ more of a tenant's writes may wait and the rest get 429. When the server's 64 places are taken,
475
+ a write from a tenant with fewer writes in flight than the one with the most takes the place of
476
+ that tenant's last write, which gets 503 `server_busy`. One that has waited two seconds gets 429
477
+ while its tenant's own write is still running, otherwise 503 `server_busy`. An application API
478
+ maintenance run waits the same way for slots of its own: one per tenant, `maintenance_runs` (1)
479
+ per server.
480
+ - `createSlotlockAgentServer` takes `subscriptions.maxPerTenant`, which `slotlock serve` sets from
481
+ `tenant_subscriptions`.
482
+ - `slotlock serve` logs a `refusals` line each minute there are any, and at shutdown: auth
483
+ refusals, rate limits, lookup overflows, busy series writes, busy maintenance runs and bodies too
484
+ large, with clients named only by per-process pseudonyms.
485
+ - `createSlotlockNodeServer` hands the fetch target the connection's remote address as a second
486
+ argument.
487
+ - `parseICalendarFeed` reads a feed series by series and names the series it refuses, with why,
488
+ instead of refusing the whole calendar; the worker pool offers it too.
489
+ `recurrenceRuleProblem(rule)` says why a rule shape is refused, and
490
+ `storedCalendarEventProblem(event)` why a stored event would be.
491
+ - An exported calendar carries a VTIMEZONE for each IANA zone it names, so a reader that does not
492
+ know the name reads the times as Slotlock does.
493
+ - The npm package ships `openapi.json`, the REST API's OpenAPI 3.1 document, exported as
494
+ `slotlock/openapi.json`. The capability manifest names the 0.2 surfaces: `rest-api`,
495
+ `application-api`, `api-keys`, `dashboard` and `maintenance`.
496
+ - The five language clients are published with each release: `slotlock` on PyPI and crates.io,
497
+ `Slotlock.Client` on NuGet, `io.pylota:slotlock` on Maven Central and the Go module's
498
+ `clients/go/v<version>` tag, each after the release suite passes and a maintainer approves. They
499
+ now carry the package's version (they said 0.1.0 beside a 0.2.0 server).
500
+ `docs/release/clients.md` has each registry's one-time setup.
501
+ - `scripts/check-stage.sh <stage-id>`, which a maintainer runs before `npm stage approve`. It
502
+ refuses a stage whose tarball is not the one the release tested, whose dist-tag would move
503
+ `latest` backwards, or whose runtime dependencies, installed as a consumer would that day, have a
504
+ high or critical advisory. 0.2.0 went public with zod 4.6.5 after `main` had moved past it.
505
+ - A known-issues notice for 0.2.0: in the README, on the site (`/docs/known-issues/`) and as text
506
+ for the v0.2.0 release notes, a pinned issue and two security advisories
507
+ (`docs/release/0.2.0-known-issues.md`). It names the six ways 0.2.0 can accept a double booking
508
+ around daylight-saving changes and series ends, and the zod pin with its workaround.
509
+ `docs/release/deprecations.md` gives the `npm deprecate` and `mcp-publisher status` commands for
510
+ 0.0.0-stage, 0.1.0, 0.1.1 and 0.2.0.
511
+ - For hosted Slotlock: drafts of a privacy notice (`PRIVACY.md`), Terms of Service (`TERMS.md`,
512
+ with data processing terms) and an Acceptable Use Policy (`ACCEPTABLE-USE.md`), each marked for
513
+ owner and legal review; a service page (`docs/hosted/README.md`) with addresses, price, limits
514
+ and contacts; `VERSIONING.md`, what `/v1`, the tools and the version numbers promise; a public
515
+ status from an off-server probe (`docs/hosted/status.md`, off until the owner sets
516
+ `SLOTLOCK_STATUS_PROBE`); and `.github/SUPPORT.md`, the route for each kind of request.
517
+
518
+ ### Changed
519
+
520
+ - A recurring event occupies both readings of each occurrence across a clock change: RFC 5545's
521
+ exact duration from its start, and its written end on the wall clock. A start in a skipped hour
522
+ moves forward by the gap, and one in a repeated hour takes the first instant. One-off events,
523
+ overrides and PERIOD RDATEs keep their exact times. Feeds, the trusted importer and the store all
524
+ read events this way.
525
+ - A stored series is expanded from the instants it holds, and its exceptions name occurrences by
526
+ UTC instant (exported as UTC `RECURRENCE-ID`s).
527
+ - A series' occurrences never overlap each other: one that reaches past the next one's start ends
528
+ there, and the next ends at the later of the two ends.
529
+ - Recurring series are stored for up to 369 days (`SLOTLOCK_MATERIALIZATION_WINDOW_DAYS`), so a
530
+ full 367-day search can be complete; search, free/busy and coverage windows keep their 367-day
531
+ cap.
532
+ - A booking, hold or event first extends each series on its resource that is not stored as far as
533
+ it reaches, so it meets their occurrences. A write past the rolling horizon on a resource whose
534
+ series goes on past it is refused with `recurrence_not_materialized`; a series known to have
535
+ ended does not count.
536
+ - A confirmed hold becomes a booking of its tenant whose reference is the hold's id, at revision 1,
537
+ so it can be read, moved and cancelled like any booking (`slotlock_confirm_hold` says so).
538
+ `createReservation` on a tenant's resource does the same. The migration, and maintenance for
539
+ rows a 0.2 server writes during a rolling deploy, give older such bookings their tenant.
540
+ - `server.json` (the MCP Registry entry) no longer marks `SLOTLOCK_AUTH_TOKEN` and
541
+ `SLOTLOCK_CONFIRMATION_SECRET` as required, and its Authorization header takes the token or an
542
+ API key.
543
+ - `slotlock --help` describes the server token as read and write, never admin.
544
+ - Releases: npm's `latest` dist-tag, and the image's `latest` tag, never move backwards. A release
545
+ on an older line takes `latest-<major>.<minor>` and a prerelease `next`, and a release stops when
546
+ the registry cannot say which version is `latest`. The image and the MCP Registry entry are
547
+ published only after the release's own suite passes, each behind an approval; the image's
548
+ attestation is still signed by `image.yml`. The packed NOTICE is written at build time from the
549
+ lockfile, so a dependency bump no longer breaks the release checks. **Release a version** takes
550
+ `dry_run`.
551
+ - Repository: CI runs the database suites on PostgreSQL 16, 17 and 18 and the Compose quickstart
552
+ end to end, and one check, "CI passed", gathers every job, the language clients included.
553
+ `scripts/configure-github.sh` requires it and DCO's "Signed-off-by" on `main` with no admin
554
+ bypass, puts the person who runs it as reviewer on the publishing environments, and sets up
555
+ CodeQL and Dependabot for every manifest.
556
+
557
+ ### Fixed
558
+
559
+ - 0.2.0's six double-booking cases (Known issues in the README): a first occurrence spanning a
560
+ clock change, the repeated hour of an autumn change in stored series, exceptions in a skipped
561
+ hour, occurrences starting in a skipped hour, a timed series with a date-only `UNTIL` (now its
562
+ whole day on the series' clock), and an iCalendar `DURATION` counting hours across a spring
563
+ change.
564
+ - A weekly-hours window whose edge falls in a skipped hour stands where the gap ends, so it holds
565
+ only the wall times it names that exist. It used to move, or give nothing.
566
+ - A yearly rule on a day some years lack (29 February) occurs in the years that have it instead of
567
+ being refused.
568
+ - A feed's daily or weekly series from years ago is searched from near the window, so a daily
569
+ standup since 2014 no longer refuses its feed. A CalDAV server's expanded series whose first
570
+ instance has no `RECURRENCE-ID` keeps its later instances busy.
571
+ - A series whose DTEND falls in a skipped hour no longer holds an extra hour every later day.
572
+ - Series the roll cannot extend (in conflict or over the budget) no longer keep a healthy series
573
+ from rolling, however many there are. The roll puts each behind for an hour and takes those it
574
+ has never put back first; its cursor carries that place (`rollRetryAt`), and a pass says when it
575
+ stopped before them (`rollPending`), so `slotlock serve` and `slotlock maintain` go round again
576
+ at once. The roll names the series it could not extend because of a collision
577
+ (`conflictEventIds`).
578
+ - A refused update of a recurring event names the booking in its way, not its own first
579
+ occurrence.
580
+ - The quickstart: `.env.example` turns the REST API on, as `slotlock serve` and Compose do, and
581
+ the docs say PostgreSQL 16 or newer (Compose ships 18).
582
+ - Documentation: SPEC.md and the README describe 0.2 and later, and SPEC.md gains a daylight-saving
583
+ section; the README says which spelling of an availability rule each surface takes
584
+ (`startMinutes` for `slotlock hours set` and `SLOTLOCK_AVAILABILITY`, `start_minutes` over
585
+ REST); the site names the version to install and has pages for the clients, operations and the
586
+ hosted service.
587
+
6
588
  ## 0.2.0 - 2026-10-23
7
589
 
8
590
  ### Added