slotlock 0.7.0 → 0.9.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.
- package/CHANGELOG.md +281 -0
- package/agent-server.d.ts +25 -5
- package/agent-server.js +152 -7
- package/agent-store-backend.d.ts +6 -1
- package/agent-store-backend.js +861 -55
- package/application-api.d.ts +19 -1
- package/application-api.js +578 -21
- package/approvals.d.ts +2 -0
- package/approvals.js +12 -5
- package/assistant.d.ts +64 -9
- package/assistant.js +450 -82
- package/availability-store.d.ts +193 -0
- package/availability-store.js +558 -0
- package/booking-rules.d.ts +92 -5
- package/booking-rules.js +243 -15
- package/dashboard-assistant-ui.d.ts +21 -0
- package/dashboard-assistant-ui.js +539 -0
- package/dashboard-markdown.d.ts +2 -0
- package/dashboard-markdown.js +157 -0
- package/dashboard-settings-ui.d.ts +1 -0
- package/dashboard-settings-ui.js +78 -0
- package/dashboard.d.ts +84 -11
- package/dashboard.js +1654 -384
- package/ddl.d.ts +5 -1
- package/ddl.js +760 -9
- package/event-feed.d.ts +1 -1
- package/event-feed.js +18 -0
- package/holiday-import.d.ts +38 -0
- package/holiday-import.js +57 -0
- package/holidays.d.ts +38 -0
- package/holidays.js +150 -0
- package/hours.d.ts +125 -0
- package/hours.js +224 -0
- package/index.d.ts +14 -3
- package/index.js +10 -2
- package/interop.d.ts +25 -7
- package/interop.js +145 -17
- package/lifecycle.d.ts +31 -0
- package/lifecycle.js +115 -0
- package/maintenance.d.ts +19 -0
- package/maintenance.js +55 -1
- package/metrics.js +3 -0
- package/openapi.json +16237 -7795
- package/package.json +1 -1
- package/ranges.d.ts +28 -0
- package/ranges.js +135 -0
- package/resource-search.d.ts +19 -0
- package/resource-search.js +153 -0
- package/rest-api.js +7 -0
- package/rules.d.ts +2 -0
- package/rules.js +2 -0
- package/self-host.d.ts +1 -1
- package/self-host.js +221 -21
- package/slotlock.manifest.json +1 -1
- package/store.d.ts +367 -24
- package/store.js +1567 -97
- package/types.d.ts +198 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,287 @@
|
|
|
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.9.0 - 2026-11-11
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- Each hold's payment is kept apart from the hold (new table `hold_payments`, under RLS, erased with
|
|
11
|
+
the tenant), so `slotlock_mark_hold_paid` with the same payment answers `replayed: true` however
|
|
12
|
+
the hold ended: confirmed in place or booked as an event. Before, a confirmed hold answered
|
|
13
|
+
`404 hold_not_found`, which `@slotlock/payments-stripe` with `refundRefused` refunded: a customer
|
|
14
|
+
reloading the return page, or Stripe redelivering a webhook, got a booked time refunded (found by
|
|
15
|
+
the security audit). The adapter now refunds only `hold_expired`, `payment_conflict` and
|
|
16
|
+
`payment_amount_mismatch`.
|
|
17
|
+
- Three integration packages in `packages/` (RFC 0004 P3), each its own process with its own
|
|
18
|
+
schema and secrets, talking to Slotlock over its API:
|
|
19
|
+
- `@slotlock/sync` (C1, C4, T2): busy time from Google freeBusy, Microsoft getSchedule and
|
|
20
|
+
iCalendar URLs, recorded as each source's busy time; confirmed bookings written back to Google
|
|
21
|
+
and Microsoft calendars, times only. OAuth with PKCE, then a signed page where the tenant's person
|
|
22
|
+
chooses which calendar blocks which resource; tokens, URLs and keys sealed with AES-256-GCM. A
|
|
23
|
+
booking changed in the calendar is reported on the feed (`coverage.conflict`), never applied. A
|
|
24
|
+
provider's 429 or 503 is retried when it says, else with backoff; a feed cursor that expired
|
|
25
|
+
while the sync was down is recovered by reconciling each calendar with the bookings as they are.
|
|
26
|
+
- `@slotlock/notify` (C2): confirmations, moves, cancellations and reminders by Postmark,
|
|
27
|
+
Resend or SES, and Twilio SMS and WhatsApp, each send recorded under its key (change id,
|
|
28
|
+
channel, recipient) before it is made. Consent per channel; test tenants go to a capture inbox.
|
|
29
|
+
Recipients are kept only as digests, except an event's attendees, kept sealed until a day after
|
|
30
|
+
it ends so a cancellation reaches them.
|
|
31
|
+
- `@slotlock/payments-stripe` (C3, T2): a PaymentIntent read back from the tenant's own Stripe
|
|
32
|
+
account marks its hold paid, with the amount received; a payment for an expired hold is refused
|
|
33
|
+
and, if asked, refunded.
|
|
34
|
+
- Each runs as its own process (`slotlock-sync`, `slotlock-notify`, `slotlock-payments-stripe`,
|
|
35
|
+
each with `migrate` and `serve`), and `packages/integrations.Dockerfile` builds one image with
|
|
36
|
+
all three.
|
|
37
|
+
- What integrations build on (RFC 0004 P3, D1's recommendation: core keeps primitives, packages
|
|
38
|
+
outside it send and sync). Feed type `reminder.due`: maintenance puts each event reminder on the
|
|
39
|
+
feed once, when it comes due (within the last day, before the event starts), and again if the
|
|
40
|
+
occurrence moves; `reminders_due` in its answer. A change's `id` is the key a sender sends it
|
|
41
|
+
once by. Events take `notify` (`slotlock_create_event`'s `notify: false`): no reminders, and a
|
|
42
|
+
sender tells no one. Reservations take `reminder_minutes` (up to five, minutes before the start),
|
|
43
|
+
each said once as `reminder.due` for the contact the reservation names, and again if it moves.
|
|
44
|
+
- Contacts (P3, T2): new table `contacts`, the people a booking may name to be told about it, by
|
|
45
|
+
the tenant's own reference, with email, phone and WhatsApp numbers (E.164), locale and consent
|
|
46
|
+
per channel. `slotlock_put_contact`, `slotlock_get_contact`, `slotlock_delete_contact`
|
|
47
|
+
(`/v1/contacts/{contact_ref}`); deleting one also takes its reference off every booking. At most
|
|
48
|
+
100,000 a tenant. `slotlock_list_contacts` (`GET /v1/contacts`) lists them a page at a time, and
|
|
49
|
+
each answers `consent_at`: when each channel's answer was given, kept while it stands. The
|
|
50
|
+
dashboard's Contacts page lists them and forgets one, for an erasure request. Reservations take
|
|
51
|
+
`contact_ref` and `notify`, both on the feed with each change; neither is ever the person.
|
|
52
|
+
- Holds that wait for payment (RFC 0004 C3, T2): `payment_required` on `slotlock_acquire_hold`
|
|
53
|
+
(time to live at most 30 minutes). Confirming one answers `409 payment_required` until
|
|
54
|
+
`slotlock_mark_hold_paid` (`POST /v1/holds/{hold_id}/paid`) records the payment's reference at
|
|
55
|
+
the integrator's processor: the same reference again answers the same hold, another
|
|
56
|
+
`payment_conflict`, an expired hold `hold_expired`. New feed type `hold.paid`. Slotlock records
|
|
57
|
+
payment state and never moves money. A hold may name what the payment must be (`payment`:
|
|
58
|
+
`amount_minor` and `currency`); `slotlock_mark_hold_paid` then takes what was `received`, and
|
|
59
|
+
less, or another currency, is `409 payment_amount_mismatch`. Holds answer `payment_amount` and
|
|
60
|
+
`paid_amount`. Agents' `slotlock_hold_slots` takes `payment_required` and `payment` too.
|
|
61
|
+
- Busy time from calendars that give no events (RFC 0004 C1, T2): `slotlock_put_source_busy`
|
|
62
|
+
(`PUT /v1/resources/{resource_id}/coverage/{source}/busy`) records a source's coverage of a
|
|
63
|
+
window and the busy time in it (at most 5,000 intervals, merged), replacing what it said of that
|
|
64
|
+
window. New table `source_busy`. Every read counts it (free/busy, a resource's busy time,
|
|
65
|
+
searches and waitlist offers), and every booking Slotlock makes is refused
|
|
66
|
+
over it (`overlap`). A confirmed booking the source newly reports busy time over is left alone
|
|
67
|
+
and reported once as `coverage.conflict` (named by `source_id`, never the source). Retiring the
|
|
68
|
+
source forgets its busy time. `slotlock_report_coverage_conflict`
|
|
69
|
+
(`POST /v1/resources/{resource_id}/coverage/{source}/conflicts`) lets a calendar sync report a
|
|
70
|
+
booking it wrote that was moved (`changed_in_calendar`) or removed (`removed_from_calendar`) in
|
|
71
|
+
the calendar, as `coverage.conflict` with the booking left as it is.
|
|
72
|
+
- Hours that inherit (RFC 0004 B2). The tenant and each location store weekly hours
|
|
73
|
+
(`setTenantAvailability`, the new `setLocationAvailability`; `PUT /v1/availability`,
|
|
74
|
+
`PUT /v1/locations/{location}/availability`, `GET /v1/availability`). A resource without its own
|
|
75
|
+
uses its location's (the nearest up the chain), then the tenant's, then the server's, and a
|
|
76
|
+
resource added later inherits too. Resources carry `inherited_availability`. The dashboard has
|
|
77
|
+
"Your hours" and an Hours page for each location; the CLI has `hours set --location`.
|
|
78
|
+
- Closures and special days (RFC 0004 B1, T2): new table `availability_exceptions`, for the tenant,
|
|
79
|
+
a location (and everything inside it) or one resource. Each replaces whole local days, read in
|
|
80
|
+
each resource's time zone: `closed` empties them, `open` gives them 1-4 windows of their own.
|
|
81
|
+
The nearest scope wins on a day. One evaluator (`openWindows`) serves search, pool availability,
|
|
82
|
+
approval policies' `outside_hours`, and writes under `enforceHours`. At most 5,000 a tenant
|
|
83
|
+
(`availability_exceptions_limit_reached`). API: list, create and delete under
|
|
84
|
+
`/v1/availability/exceptions`. Dashboard: a Closures page. CLI: `slotlock closures
|
|
85
|
+
list|add|delete|import`.
|
|
86
|
+
- Holiday calendars (B1): `slotlock_import_holidays` (`POST /v1/availability/holidays`) and
|
|
87
|
+
`closures import` close the days an iCalendar file's all-day events cover, recurring ones
|
|
88
|
+
expanded over a bounded span. `serve` fetches an `https` URL through the guarded fetch (no
|
|
89
|
+
private addresses, 1 MB, 10 seconds); the store-backed application backend fetches none unless
|
|
90
|
+
given `holidayFetch`. Each import is a calendar (new table `holiday_calendars`, under RLS, erased
|
|
91
|
+
with the tenant), kept by name where it applies: its `name`, else its own `X-WR-CALNAME`, else
|
|
92
|
+
the URL. Importing it again updates its closures and removes those it no longer names in the
|
|
93
|
+
span (`removed`), so a holiday a calendar dropped goes too; a closure imported before calendars
|
|
94
|
+
were kept becomes the calendar's that names it. With `subscribe`, the URL is kept and maintenance
|
|
95
|
+
imports it again every 30 days (`holidayFetch` on `maintainSlotlockTenant` and the scheduler;
|
|
96
|
+
`serve` and `slotlock maintain` pass the guarded fetch), a failed import keeping its closures,
|
|
97
|
+
noting why (`last_error`) and trying again a day later. `slotlock_list_holiday_calendars`
|
|
98
|
+
(`GET /v1/availability/holiday-calendars`) and `slotlock_delete_holiday_calendar`
|
|
99
|
+
(`DELETE …/{calendar_id}`, its closures with it); closures answer `calendar_id`; at most 50
|
|
100
|
+
calendars a tenant. The dashboard's Closures page takes a file upload (the form was limited to
|
|
101
|
+
8 KB, too small for most calendars, and now takes 1 MB), a paste or, where the server fetches
|
|
102
|
+
them (`serve`, Cloud), an address with "Import it again every 30 days", and lists the calendars
|
|
103
|
+
with their next import, a failed one's error, and Delete. `closures import` names the calendar
|
|
104
|
+
with `--label`.
|
|
105
|
+
- Durations and hires (RFC 0004 B3). Booking rules gain `minDurationMinutes`, `maxDurationMinutes`,
|
|
106
|
+
`allowedDurations` and `defaultDurationMinutes`, and for a booking from one local day to another
|
|
107
|
+
`pickupTimes` and `returnTimes` (it starts and ends in them, and under `enforceHours` needs only
|
|
108
|
+
its start and end inside the hours). `turnaroundMinutes` goes up to a week. Refusals:
|
|
109
|
+
`booking_rule_min_duration`, `booking_rule_max_duration`, `booking_rule_duration_not_allowed`,
|
|
110
|
+
`booking_rule_pickup_time`, `booking_rule_return_time`. Searches offer only allowed lengths, and
|
|
111
|
+
take `defaultDurationMinutes` when they name none (`400 duration_required` otherwise).
|
|
112
|
+
`hoursApply` (`hours_apply`) says how the hours hold a hire: `whole` (all of it) or `ends` (its
|
|
113
|
+
start and end only), by default `ends` with pick-up or return times, else `whole`. Writes follow
|
|
114
|
+
it under `enforceHours`; `slotlock_find_available_slots` and `slotlock_find_next_available`
|
|
115
|
+
follow it always, so with `ends` they offer slots from one day to another, judged as the write
|
|
116
|
+
would be, and `slotlock_find_available_ranges` judges ranges as the write would where hours are
|
|
117
|
+
enforced. A pick-up or return refusal comes with alternatives, as other refusals of a time do.
|
|
118
|
+
- Zone rule changes reach everything (RFC 0004 B9): `followZoneRules` also re-checks a recurring
|
|
119
|
+
series' occurrences to come (each kept with its wall time, reported once as
|
|
120
|
+
`event.timezone_shifted` with its `recurrence_id`; with `move`, the whole series moves at its
|
|
121
|
+
next revision in one transaction, each occurrence and its booking keeping its `recurrence_id`,
|
|
122
|
+
but only where the server's zone rules agree with the database's, else each is reported with
|
|
123
|
+
`reason: "rules_disagree"`; something in the way keeps its times, said once as
|
|
124
|
+
`event.timezone_conflict`), and reservations written with the new
|
|
125
|
+
optional `local_start` (`localStart`), reported as `reservation.timezone_shifted` or, with
|
|
126
|
+
`move`, moved at the next revision (`reservation.timezone_conflict` when something is in the
|
|
127
|
+
way). A `local_start` that does not name the reservation's start is `400 invalid_local_start`.
|
|
128
|
+
- Holds across resources (RFC 0004 B7, T2): `slotlock_hold_together` (`POST /v1/slot-holds/together`),
|
|
129
|
+
the 19th agent tool, holds one time on 2-20 resources all or nothing, as one group whose holds
|
|
130
|
+
expire together. `slotlock_book_together` takes its `hold_group_id` and books the set from those
|
|
131
|
+
holds in one transaction. The application API gains `slotlock_tenant_book_together`
|
|
132
|
+
(`POST /v1/tenant/bundles`) and `slotlock_tenant_book_pool` (`POST /v1/tenant/pool-bookings`), the
|
|
133
|
+
agents' bundle and pool bookings for the tenant's own systems. Their events are the tenant's
|
|
134
|
+
(owner `internal`), not the key's: no agent's event quota counts them, and revoking the key
|
|
135
|
+
leaves them. Approval policies hold them only when they say `include_application: true` (new,
|
|
136
|
+
false by default), and a grant books them as the tenant's again; a key that `requires_approval`
|
|
137
|
+
still waits for a person whatever it books.
|
|
138
|
+
- Waitlists that match (RFC 0004 B6): an entry may wait for any slot of a length within its
|
|
139
|
+
window (`duration_minutes`, `durationMs`), on a resource or a pool, and is offered the earliest
|
|
140
|
+
free one. Offers end `accepted`, `declined` (new `slotlock_decline_waitlist_offer`,
|
|
141
|
+
`POST /v1/waitlist/{entry_id}/decline`) or `lapsed`, decided by what becomes of their hold, by a
|
|
142
|
+
database trigger whatever path takes it. New feed types `waitlist.joined`, `.accepted`,
|
|
143
|
+
`.declined`, `.lapsed` and `.left`. Entries answer `duration_minutes` and `decided_at`.
|
|
144
|
+
- A written booking lifecycle (RFC 0004 B4, T2): `src/lifecycle.ts` names each status, the move
|
|
145
|
+
each action makes and the refusal every other gets, and a test holds the store to it. A
|
|
146
|
+
tombstone keeps why the booking ended (`end_reason`: `cancelled` or `no_show`). New feed types:
|
|
147
|
+
`reservation.rescheduled` (a move, as well as `reservation.updated`) and `reservation.completed`,
|
|
148
|
+
which maintenance writes once for each booking whose end passed in the last week
|
|
149
|
+
(`bookings_completed` in its answer).
|
|
150
|
+
- Cancellation and reschedule policies (RFC 0004 B5, T2): rules `cancelCutoffMinutes` and
|
|
151
|
+
`rescheduleCutoffMinutes` (`booking_rule_cancel_cutoff`, `booking_rule_reschedule_cutoff`), which
|
|
152
|
+
an admin may go past with `override_reason`, kept on the tombstone and the feed; and
|
|
153
|
+
`cancellationFee`, recorded on a late cancellation for the integrator to charge.
|
|
154
|
+
- `slotlock_find_available_ranges` (`GET /v1/available-ranges`), the 18th agent tool: hires across
|
|
155
|
+
days for named resources or a pool's members, the earliest range on each from a pick-up window,
|
|
156
|
+
lasting N days or ending in a return window.
|
|
157
|
+
- VAVAILABILITY carries RDATE and EXDATE (B1): the export adds the next year's closures as EXDATEs
|
|
158
|
+
and special hours as AVAILABLEs with RDATEs. The import reads them, and single AVAILABLEs, as
|
|
159
|
+
the resource's own exceptions, replacing those an earlier import made.
|
|
160
|
+
|
|
161
|
+
- **Chats in the dashboard assistant.** Each conversation is a chat a member keeps:
|
|
162
|
+
- it is titled by its first message, at its own address (`<root>/assistant?c=<id>`);
|
|
163
|
+
- chats are listed beside the one shown, most recent first, grouped by day (Today, Yesterday,
|
|
164
|
+
Previous 7 days, Previous 30 days, Older), with a search box, and **Rename** and **Delete**
|
|
165
|
+
behind each one's menu;
|
|
166
|
+
- **New chat** starts another, and on a narrow screen the list is a page of its own
|
|
167
|
+
(`<root>/assistant/chats`);
|
|
168
|
+
- the page opens on the chat used in the past two hours, else on a new one;
|
|
169
|
+
- a streamed reply tells the page which chat it went to, so a new one gets its address and its
|
|
170
|
+
place in the list without a reload.
|
|
171
|
+
|
|
172
|
+
Chats are kept until the member deletes one, at most 100 per member (`SLOTLOCK_ASSISTANT_MAX_CHATS`;
|
|
173
|
+
past it, the least recently used goes, even with many started at once). A chat deleted while its
|
|
174
|
+
reply is being written stays deleted. They live in the new table `slotlock.assistant_chats`,
|
|
175
|
+
under forced RLS, erased with the tenant. `slotlock migrate` makes each member's 0.8 conversation
|
|
176
|
+
their first chat, once. Run `slotlock migrate`.
|
|
177
|
+
- **Finding a resource by name.** `slotlock_list_resources` takes `query` and ranks the resources
|
|
178
|
+
that match what a person calls one, best first:
|
|
179
|
+
- each word is looked for in the name, reference, labels, kind, location and description;
|
|
180
|
+
- case and accents are ignored, and a typo, a word cut short or words run together are forgiven
|
|
181
|
+
(`mercedez`, `merc 2025` and `mercedesbenz` all find "Mercedes-Benz 2025");
|
|
182
|
+
- numbers match exactly;
|
|
183
|
+
- a search does bounded work, whatever the resources hold: past a budget of typo checks, it
|
|
184
|
+
matches exactly, by prefix and by words run together only.
|
|
185
|
+
|
|
186
|
+
`resource_not_found` for a name or reference given where an id belongs names the likeliest
|
|
187
|
+
resources in `details.candidate_1` to `candidate_3`.
|
|
188
|
+
|
|
189
|
+
### Changed
|
|
190
|
+
|
|
191
|
+
- The tenant's hours are stored, not copied onto every resource (RFC 0004 B2), and resources
|
|
192
|
+
without their own inherit them, those added later too. `storeTenantAvailability` stores them and
|
|
193
|
+
answers `{ inheriting, ownHours }`. `setTenantAvailability` keeps its 0.8.0 contract, deprecated:
|
|
194
|
+
every resource uses the hours afterwards, and it answers how many. `PUT /v1/availability` keeps
|
|
195
|
+
the hours resources have of their own by default; its `override_resources` is `none`, `all`
|
|
196
|
+
(clear them) or `matching` (clear those the same as the tenant's were or as the new ones: the
|
|
197
|
+
copies earlier versions made, while hours set apart on purpose stay), as are
|
|
198
|
+
`hours set --all --override` and `--matching`. It answers `resources_updated` (resources the
|
|
199
|
+
hours now apply to) and `resources_with_own_hours`. The dashboard's "Your hours" names the
|
|
200
|
+
resources that keep their own, with the same three choices, and `/resources?hours=own` lists
|
|
201
|
+
them.
|
|
202
|
+
- `slotlock_run_maintenance` answers `bookings_completed`; the five clients' checks of its keys
|
|
203
|
+
include it. `slotlock_cancel_reservation` and `slotlock_cancel_tenant_event` answer
|
|
204
|
+
`cancellation_fee` (null when none).
|
|
205
|
+
- `parseVAvailability` answers `days` as well as `rules`, and no longer refuses RDATE, EXDATE or an
|
|
206
|
+
AVAILABLE without RRULE.
|
|
207
|
+
|
|
208
|
+
- **The dashboard assistant's store** (`SlotlockAssistantStore`) keeps chats. `listChats`,
|
|
209
|
+
`getChat`, `putChat`, `renameChat` and `deleteChat` replace `getConversation`,
|
|
210
|
+
`putConversation` and `deleteConversation`, and `settleAction` takes the chat's id. The tenant
|
|
211
|
+
store has `listAssistantChats`, `getAssistantChat`, `putAssistantChat`, `renameAssistantChat`
|
|
212
|
+
and `deleteAssistantChat`, and `settleAssistantAction` takes `chatId`.
|
|
213
|
+
- **Conversation lifetime.** `conversationTtlMs` no longer defaults to 2 hours: without it, chats
|
|
214
|
+
are kept until deleted. Given, it may be up to 3,650 days.
|
|
215
|
+
- **New conversation is New chat.** It starts another chat and deletes nothing. A form of 0.8 that
|
|
216
|
+
posts `<root>/assistant/reset` does the same.
|
|
217
|
+
- **The assistant's system prompt** lists the workspace's resources (up to 50, with the ids tools
|
|
218
|
+
take) and says how to find others. The tool descriptions and the MCP instructions say that
|
|
219
|
+
`resource_id` takes an id and how to find one, and that a filter leaves out resources that state
|
|
220
|
+
nothing.
|
|
221
|
+
|
|
222
|
+
### Fixed
|
|
223
|
+
|
|
224
|
+
- Enforced hours (`enforceHours`) judged a write inside `withTenant` against no hours when its
|
|
225
|
+
resource used the server's default (`defaultAvailabilityRules`, `SLOTLOCK_AVAILABILITY` under
|
|
226
|
+
`serve`): every such booking was refused `booking_rule_outside_hours`. The tenant's store now
|
|
227
|
+
carries the server's hours.
|
|
228
|
+
|
|
229
|
+
- **The assistant's chips stream.** "What's on today" and "Find free time" were forms that waited
|
|
230
|
+
for the whole turn and then reloaded the page; they now send through the box, so their reply
|
|
231
|
+
streams.
|
|
232
|
+
|
|
233
|
+
- The dashboard assistant's Stop stops the turn on any runtime. Cancelling the streamed reply now
|
|
234
|
+
aborts the turn itself, which keeps what it had. Before, only `request.signal` stopped it, and a
|
|
235
|
+
runtime that does not abort it when the client goes kept the turn running. On Workers, the
|
|
236
|
+
request's database connection now closes only once the stopped turn is kept.
|
|
237
|
+
- The Overview never shows a change by its type. A change it has no words for, from a newer server,
|
|
238
|
+
reads "Something changed", on its resource when it names one. A type such as `constructor` no
|
|
239
|
+
longer finds an inherited entry.
|
|
240
|
+
- The assistant page's script keeps live steps in a map with no prototype, so a provider's tool-call
|
|
241
|
+
id of `__proto__` names nothing.
|
|
242
|
+
|
|
243
|
+
## 0.8.0 - 2026-11-10
|
|
244
|
+
|
|
245
|
+
### Added
|
|
246
|
+
|
|
247
|
+
- **Usage** in the dashboard's navigation on hosted Slotlock, with or without Stripe: the plan (or
|
|
248
|
+
the default it moves to, and when) against its limits, this month's operations, bookings and
|
|
249
|
+
holds against the quotas, and which key used what. Without Stripe, an owner asks for another plan
|
|
250
|
+
at `SLOTLOCK_PLAN_CONTACT`. `createSlotlockDashboard` takes `plans` (`plan`, `enrol`,
|
|
251
|
+
`planDefault`, `contact`); `billing` still brings its own.
|
|
252
|
+
|
|
253
|
+
### Changed
|
|
254
|
+
|
|
255
|
+
- The Overview leads with what needs a person (approvals waiting, a failed payment, a quota nearly
|
|
256
|
+
or fully used, a coming plan the workspace is over), then the week ahead and the activity as
|
|
257
|
+
sentences ("Booking agent booked room-3", passive for what Slotlock did, repeats counted on one
|
|
258
|
+
line), never a change's type. Get started walks a new workspace from its first resource to an
|
|
259
|
+
agent that has used a key, folds to one line half way and goes once done. The plan's use and the
|
|
260
|
+
MCP and A2A endpoints (with the Claude Code command) sit beside the activity.
|
|
261
|
+
- The Plan page is Usage, and a limit refused says "see Usage".
|
|
262
|
+
|
|
263
|
+
- The dashboard assistant is a chat. A reply streams into the page as the model writes it (the
|
|
264
|
+
dashboard's own script reads `POST <root>/assistant/stream`, one JSON object a line; the CSP
|
|
265
|
+
allows `connect-src 'self'` for it), with a Stop button. Without script, a message still posts
|
|
266
|
+
and the page shows the reply. Replies are Markdown, rendered by one renderer on the server and,
|
|
267
|
+
from its source, in the page as they stream: everything escaped first, links http(s) only. The
|
|
268
|
+
page is one centred column, with the box at the centre when the conversation is empty and five
|
|
269
|
+
things to ask beside it. Each run of reads folds into one line ("Read resources, checked
|
|
270
|
+
availability") that names, in words, a step that could not run and why. A proposed change is a
|
|
271
|
+
card that leads with when, which resource and its title, and keeps every other argument one click
|
|
272
|
+
away; its button says what it does ("Book", "Cancel booking"), and once decided it folds into
|
|
273
|
+
one line. A provider failure ends the conversation with Retry.
|
|
274
|
+
- Settings is a page a section (Assistant, Endpoints, Hours, Account). The assistant's section
|
|
275
|
+
shows what is connected, with Test and its result in place, then one form: the provider as tiles
|
|
276
|
+
(a named one brings its own URL and API format; Custom takes both), the model with Load models,
|
|
277
|
+
and the key as its last four characters with Replace. Disconnect stands apart.
|
|
278
|
+
|
|
279
|
+
### Fixed
|
|
280
|
+
|
|
281
|
+
- A message sent twice (a double click, a reload) no longer ends on "That form was already
|
|
282
|
+
sent": the second lands on the conversation, and the turn runs once. The same holds for
|
|
283
|
+
approving, declining and New conversation.
|
|
284
|
+
- A success notice (Saved, Connected) was shown in the error colour: notices are alerts only on
|
|
285
|
+
an error page, and a status otherwise.
|
|
286
|
+
|
|
6
287
|
## 0.7.0 - 2026-11-10
|
|
7
288
|
|
|
8
289
|
### Added
|
package/agent-server.d.ts
CHANGED
|
@@ -39,6 +39,8 @@ export declare const SLOTLOCK_A2A_TASK_PAGE_SIZE: Readonly<{
|
|
|
39
39
|
}>;
|
|
40
40
|
/** The A2A 1.0 `TaskState` names, as a `ListTasks` status filter spells them in JSON. */
|
|
41
41
|
export declare const SLOTLOCK_A2A_TASK_STATES: readonly ["TASK_STATE_UNSPECIFIED", "TASK_STATE_SUBMITTED", "TASK_STATE_WORKING", "TASK_STATE_COMPLETED", "TASK_STATE_FAILED", "TASK_STATE_CANCELED", "TASK_STATE_INPUT_REQUIRED", "TASK_STATE_REJECTED", "TASK_STATE_AUTH_REQUIRED"];
|
|
42
|
+
/** The longest resource search (resource-search.ts SLOTLOCK_RESOURCE_SEARCH_MAX; a test holds them equal). */
|
|
43
|
+
export declare const SLOTLOCK_AGENT_RESOURCE_SEARCH_MAX = 200;
|
|
42
44
|
/** The tools' copy of the profile's kinds and bounds, for the test that holds it to resource-profile.ts. */
|
|
43
45
|
export declare const SLOTLOCK_AGENT_RESOURCE_PROFILE: Readonly<{
|
|
44
46
|
kinds: ("desk" | "equipment" | "machine" | "other" | "person" | "room" | "service" | "space" | "vehicle")[];
|
|
@@ -62,7 +64,7 @@ export declare const SLOTLOCK_MAX_ALTERNATIVES = 3;
|
|
|
62
64
|
* Operation (MCP tool / A2A skill) names. They use only `[a-z_]`, so they are valid tool names for the
|
|
63
65
|
* Claude Messages API (`^[a-zA-Z0-9_-]{1,128}$`), OpenAI function calling and every MCP host.
|
|
64
66
|
*/
|
|
65
|
-
export type SlotlockAgentOperation = 'slotlock_list_resources' | 'slotlock_get_free_busy' | 'slotlock_find_next_available' | 'slotlock_find_available_slots' | 'slotlock_hold_slots' | 'slotlock_release_slots' | 'slotlock_confirm_slot' | 'slotlock_reschedule' | 'slotlock_cancel' | 'slotlock_book_together' | 'slotlock_book_pool' | 'slotlock_get_pool_availability' | 'slotlock_create_event' | 'slotlock_get_event' | 'slotlock_list_events' | 'slotlock_update_event' | 'slotlock_delete_event';
|
|
67
|
+
export type SlotlockAgentOperation = 'slotlock_list_resources' | 'slotlock_get_free_busy' | 'slotlock_find_next_available' | 'slotlock_find_available_slots' | 'slotlock_find_available_ranges' | 'slotlock_hold_slots' | 'slotlock_hold_together' | 'slotlock_release_slots' | 'slotlock_confirm_slot' | 'slotlock_reschedule' | 'slotlock_cancel' | 'slotlock_book_together' | 'slotlock_book_pool' | 'slotlock_get_pool_availability' | 'slotlock_create_event' | 'slotlock_get_event' | 'slotlock_list_events' | 'slotlock_update_event' | 'slotlock_delete_event';
|
|
66
68
|
/**
|
|
67
69
|
* Names earlier builds published, which every entry point still accepts and dispatches as the same
|
|
68
70
|
* operation; nothing advertises them. The dotted names came first, before tool names had to be
|
|
@@ -189,6 +191,12 @@ export interface SlotlockAgentCalendarBackend {
|
|
|
189
191
|
* (`slotlock_hold_slots`); a tool without it answers `holds_unavailable`.
|
|
190
192
|
*/
|
|
191
193
|
holdSlots?(context: SlotlockAgentInvocationContext, input: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
194
|
+
/**
|
|
195
|
+
* Optional. Hold one time on 2-20 resources all or nothing, as one group with one time to live
|
|
196
|
+
* (`slotlock_hold_together`, RFC 0004 B7), else `holds_unavailable`. `slotlock_book_together`
|
|
197
|
+
* with its `hold_group_id` books the set.
|
|
198
|
+
*/
|
|
199
|
+
holdTogether?(context: SlotlockAgentInvocationContext, input: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
192
200
|
/** Optional. Book any one free member of a pool (`slotlock_book_pool`), else `pools_unavailable`. */
|
|
193
201
|
bookPool?(context: SlotlockAgentInvocationContext, input: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
194
202
|
/**
|
|
@@ -228,6 +236,18 @@ export interface SlotlockAgentCalendarBackend {
|
|
|
228
236
|
}[];
|
|
229
237
|
certain: boolean;
|
|
230
238
|
}>;
|
|
239
|
+
/**
|
|
240
|
+
* Optional. Hires across days (`slotlock_find_available_ranges`, RFC 0004 B3): each resource's, or
|
|
241
|
+
* each pool member's, earliest range from a pick-up window to a return window, else
|
|
242
|
+
* `ranges_unavailable`.
|
|
243
|
+
*/
|
|
244
|
+
findAvailableRanges?(context: SlotlockAgentInvocationContext, input: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
245
|
+
/**
|
|
246
|
+
* Optional. The length a search for these resources offers when it is asked for none: their
|
|
247
|
+
* booking rules' `defaultDurationMinutes` when they all give the same one, else null. Without it,
|
|
248
|
+
* a search names its length.
|
|
249
|
+
*/
|
|
250
|
+
defaultDuration?(context: SlotlockAgentInvocationContext, resourceIds: readonly string[]): Promise<number | null>;
|
|
231
251
|
}
|
|
232
252
|
/**
|
|
233
253
|
* OAuth 2.0 discovery for the MCP endpoint (MCP 2025-11-25 authorization, RFC 9728). Slotlock only
|
|
@@ -593,11 +613,11 @@ export declare function slotlockOpenApi(options?: {
|
|
|
593
613
|
application?: boolean;
|
|
594
614
|
}): Record<string, unknown>;
|
|
595
615
|
export declare function slotlockAgentTools(): {
|
|
596
|
-
name: "slotlock_book_pool" | "slotlock_book_together" | "slotlock_cancel" | "slotlock_confirm_slot" | "slotlock_create_event" | "slotlock_delete_event" | "slotlock_find_available_slots" | "slotlock_find_next_available" | "slotlock_get_event" | "slotlock_get_free_busy" | "slotlock_get_pool_availability" | "slotlock_hold_slots" | "slotlock_list_events" | "slotlock_list_resources" | "slotlock_release_slots" | "slotlock_reschedule" | "slotlock_update_event";
|
|
597
|
-
title: "Book from a pool" | "Book resources together" | "Cancel an event" | "Confirm a held slot" | "Create resource event" | "Delete resource event" | "Find available slots" | "Find next available interval" | "Get free-busy" | "Get pool availability" | "Get resource event" | "Hold candidate slots" | "List calendar resources" | "List resource events" | "Release held slots" | "Reschedule an event" | "Update resource event";
|
|
598
|
-
description: "Book a resource for a time, as an event of up to 3,660 days (a booking, lease or long rental). The database refuses a time already taken: 409 overlap, with alternatives (the earliest free times of the same length) and, when the booking in the way is yours, its id. A booking rule that refuses the time answers booking_rule_… with alternatives too. To book a time you hold, pass its hold_id, or use slotlock_confirm_slot. A retry with the same idempotency_key answers the same event." | "Book a time you hold (a hold_id from slotlock_hold_slots): the held resource and time exactly, with the title and details you give, and the rest of its group released. 409 hold_expired when the hold lapsed first; 404 hold_not_found when it was released, booked or is not yours. A retry answers the same event (replayed)." | "Book any one member of a pool (a vehicle class, a room type) for a time: members are tried in the pool's order (by id, fewest bookings in the last 30 days first, or in turn, as the tenant sets it) and the first that is free and allows the booking is booked, in one transaction, so a class sells out exactly at its number of members. The event names the member booked. 409 pool_full when none is free; 404 pool_not_found when the pool has no member you may book. A retry with the same idempotency_key answers the same event." | "Book the same time on 2-20 resources at once, all or none: a van with its driver, a room with its projector. If any of them is taken, nothing is booked, and the refusal names that resource_id. Find such a time with slotlock_find_available_slots and require_free. A retry with the same idempotency_key answers the same events." | "Cancel one of your events at the revision you last read (expected_revision); a stale one is 409 revision_conflict. It cannot come back at that revision, so an old copy synced later does not restore it. slotlock_cancel does the same with fewer fields." | "Cancel one of your events at the revision you last read (expected_revision); a stale one is 409 revision_conflict. Its time is free at once. A retry answers the same cancellation (replayed)." | "Change one of your events at the revision you last read (expected_revision): its time, resource or details, all or nothing. A time already taken is 409 overlap with alternatives; a stale revision is 409 revision_conflict (read the event again). With expected_resource_id, the resource you read it on, an event since moved elsewhere is not found. To change only the time, slotlock_reschedule is simpler." | "Find the earliest certain interval that satisfies the requested duration (up to the 367-day search window, so a multi-month rental slot can be found). Name the resources with resource_ids, or select them as slotlock_get_free_busy does: at most 100." | "Hold up to five candidate slots for a short time (30-900 seconds) while a person chooses, as alternatives in one group: each free one is held, the others are listed with why not. Book one by passing its hold_id to slotlock_create_event, which releases the rest; unbooked holds expire on their own. A hold is yours alone, and you may have 20 live at once. A candidate refused for its time comes with alternatives (the earliest free times of its length). slotlock_confirm_slot books a held time." | "How many members of a pool (a vehicle class, a room type) are free across a window, each only where its booking rules let a booking start: the window split where that number changes, each part with how many are free, and coverage certainty. Book one with slotlock_book_pool. 404 pool_not_found when the pool has no member you may book." | "List resources visible to the authenticated tenant principal, with what each says about itself: its name (show it rather than the reference), description, kind, capacity and labels, where it is (location), the groups it is in (a team, a department), and the pool it belongs to (a vehicle class, a room type). With kind, only resources of that kind; with labels, only those with every label given; with location, only those there or at a location in it; with group, only that group's. With pool, only that pool's members, in the order slotlock_book_pool tries them." | "List up to `limit` certain slots of the requested duration, earliest first across the resources, each one free of busy time and inside the resource's hours, to offer a person a choice. With require_free, only times when at least that many of the resources are free together, each slot naming them in resource_ids: every resource for a booking that needs them all (book it with slotlock_book_together), or any n of them. No slot is listed unless every resource's coverage is certain. Name the resources with resource_ids, or select them as slotlock_get_free_busy does: at most 20." | "List your events that meet a time window, earliest first." | "Move one of your events to a new time, and to another resource if you give one, at the revision you last read (expected_revision). A time already taken is 409 overlap with alternatives; a stale revision 409 revision_conflict. A retry answers the same move (replayed)." | "Read one of your events by its id, with its revision (pass it as expected_revision to change or cancel it)." | "Release one of your holds, and the rest of its group, at once instead of letting them expire. `released` is false when it was already gone." | "When each resource is busy in a window, as busy intervals only (never whose or what), and whether that is certain (coverage: \"uncertain\" where a calendar feeding it has not been read lately). Name the resources with resource_ids, or select them instead by location (and every location in it), group, labels (every one), kind or pool: at most 100 (more answers selection_too_large, none selection_empty), named in selected_resource_ids.";
|
|
616
|
+
name: "slotlock_book_pool" | "slotlock_book_together" | "slotlock_cancel" | "slotlock_confirm_slot" | "slotlock_create_event" | "slotlock_delete_event" | "slotlock_find_available_ranges" | "slotlock_find_available_slots" | "slotlock_find_next_available" | "slotlock_get_event" | "slotlock_get_free_busy" | "slotlock_get_pool_availability" | "slotlock_hold_slots" | "slotlock_hold_together" | "slotlock_list_events" | "slotlock_list_resources" | "slotlock_release_slots" | "slotlock_reschedule" | "slotlock_update_event";
|
|
617
|
+
title: "Book from a pool" | "Book resources together" | "Cancel an event" | "Confirm a held slot" | "Create resource event" | "Delete resource event" | "Find available ranges" | "Find available slots" | "Find next available interval" | "Get free-busy" | "Get pool availability" | "Get resource event" | "Hold candidate slots" | "Hold resources together" | "List calendar resources" | "List resource events" | "Release held slots" | "Reschedule an event" | "Update resource event";
|
|
618
|
+
description: "Book a resource for a time, as an event of up to 3,660 days (a booking, lease or long rental). The database refuses a time already taken: 409 overlap, with alternatives (the earliest free times of the same length) and, when the booking in the way is yours, its id. A booking rule that refuses the time answers booking_rule_… with alternatives too. To book a time you hold, pass its hold_id, or use slotlock_confirm_slot. A retry with the same idempotency_key answers the same event." | "Book a time you hold (a hold_id from slotlock_hold_slots): the held resource and time exactly, with the title and details you give, and the rest of its group released. 409 hold_expired when the hold lapsed first; 404 hold_not_found when it was released, booked or is not yours. A retry answers the same event (replayed)." | "Book any one member of a pool (a vehicle class, a room type) for a time: members are tried in the pool's order (by id, fewest bookings in the last 30 days first, or in turn, as the tenant sets it) and the first that is free and allows the booking is booked, in one transaction, so a class sells out exactly at its number of members. The event names the member booked. 409 pool_full when none is free; 404 pool_not_found when the pool has no member you may book. A retry with the same idempotency_key answers the same event." | "Book the same time on 2-20 resources at once, all or none: a van with its driver, a room with its projector. If any of them is taken, nothing is booked, and the refusal names that resource_id. Find such a time with slotlock_find_available_slots and require_free. With hold_group_id, it books the set slotlock_hold_together held, from those holds. A retry with the same idempotency_key answers the same events." | "Cancel one of your events at the revision you last read (expected_revision); a stale one is 409 revision_conflict. It cannot come back at that revision, so an old copy synced later does not restore it. slotlock_cancel does the same with fewer fields." | "Cancel one of your events at the revision you last read (expected_revision); a stale one is 409 revision_conflict. Its time is free at once. A retry answers the same cancellation (replayed)." | "Change one of your events at the revision you last read (expected_revision): its time, resource or details, all or nothing. A time already taken is 409 overlap with alternatives; a stale revision is 409 revision_conflict (read the event again). With expected_resource_id, the resource you read it on, an event since moved elsewhere is not found. To change only the time, slotlock_reschedule is simpler." | "Find hires across days, such as a car from Friday to Monday: for each resource named, or each member of a pool, the earliest range that starts between pickup_start and pickup_end and either lasts `days` days (ending at the same local time) or ends between return_start and return_end. Each range is free throughout with its buffers, allowed by the resource's booking rules (pick-up and return times, lengths, notice, step) and starts and ends inside its bookable hours. Up to `limit` ranges, earliest first, one per resource; none unless every resource's coverage is certain. Book one with slotlock_create_event, or a pool's with slotlock_book_pool, at its start and end." | "Find the earliest certain interval that satisfies the requested duration (up to the 367-day search window, so a multi-month rental slot can be found). Name the resources with resource_ids, or select them as slotlock_get_free_busy does: at most 100." | "Hold one time on 2-20 resources at once, all or none, such as a car with its driver while a person decides: if any is taken, nothing is held and the refusal names that resource_id. The holds are one group with one time to live (ttl_seconds, the earliest of them). Book the set with slotlock_book_together and its hold_group_id; release them with slotlock_release_slots and any hold_id. A retry with the same idempotency_key answers the same holds." | "Hold up to five candidate slots for a short time (30-900 seconds) while a person chooses, as alternatives in one group: each free one is held, the others are listed with why not. Book one by passing its hold_id to slotlock_create_event, which releases the rest; unbooked holds expire on their own. With payment_required, a hold is booked only once the system you book for records its payment (the person pays there): booking it before then answers payment_required, so send the person to pay and try again. A hold is yours alone, and you may have 20 live at once. A candidate refused for its time comes with alternatives (the earliest free times of its length). slotlock_confirm_slot books a held time." | "How many members of a pool (a vehicle class, a room type) are free across a window, each only where its booking rules let a booking start: the window split where that number changes, each part with how many are free, and coverage certainty. Book one with slotlock_book_pool. 404 pool_not_found when the pool has no member you may book." | "List resources visible to the authenticated tenant principal, with what each says about itself: its name (show it rather than the reference), description, kind, capacity and labels, where it is (location), the groups it is in (a team, a department), and the pool it belongs to (a vehicle class, a room type). With kind, only resources of that kind; with labels, only those with every label given; with location, only those there or at a location in it; with group, only that group's. With pool, only that pool's members, in the order slotlock_book_pool tries them. With query, the resources that best match what a person calls one, best first, at most limit and with no next_cursor: each word is looked for in the name, reference, labels, kind, location and description, ignoring case and accents, and forgiving a typo, a word cut short or words run together (\"mercedez\", \"merc 2025\" and \"mercedesbenz\" find \"Mercedes-Benz 2025\"); numbers match exactly. It is the way to find the id of a resource a person names, since other tools take ids only. When several match about equally, ask which one is meant. A resource that states no kind, labels or location is left out by those filters, so search without them before saying a resource does not exist." | "List up to `limit` certain slots of the requested duration, earliest first across the resources, each one free of busy time and inside the resource's hours, to offer a person a choice. With require_free, only times when at least that many of the resources are free together, each slot naming them in resource_ids: every resource for a booking that needs them all (book it with slotlock_book_together), or any n of them. No slot is listed unless every resource's coverage is certain. Name the resources with resource_ids, or select them as slotlock_get_free_busy does: at most 20." | "List your events that meet a time window, earliest first." | "Move one of your events to a new time, and to another resource if you give one, at the revision you last read (expected_revision). A time already taken is 409 overlap with alternatives; a stale revision 409 revision_conflict. A retry answers the same move (replayed)." | "Read one of your events by its id, with its revision (pass it as expected_revision to change or cancel it)." | "Release one of your holds, and the rest of its group, at once instead of letting them expire. `released` is false when it was already gone." | "When each resource is busy in a window, as busy intervals only (never whose or what), and whether that is certain (coverage: \"uncertain\" where a calendar feeding it has not been read lately). Name the resources with resource_ids, or select them instead by location (and every location in it), group, labels (every one), kind or pool: at most 100 (more answers selection_too_large, none selection_empty), named in selected_resource_ids.";
|
|
599
619
|
annotations: {
|
|
600
|
-
title: "Book from a pool" | "Book resources together" | "Cancel an event" | "Confirm a held slot" | "Create resource event" | "Delete resource event" | "Find available slots" | "Find next available interval" | "Get free-busy" | "Get pool availability" | "Get resource event" | "Hold candidate slots" | "List calendar resources" | "List resource events" | "Release held slots" | "Reschedule an event" | "Update resource event";
|
|
620
|
+
title: "Book from a pool" | "Book resources together" | "Cancel an event" | "Confirm a held slot" | "Create resource event" | "Delete resource event" | "Find available ranges" | "Find available slots" | "Find next available interval" | "Get free-busy" | "Get pool availability" | "Get resource event" | "Hold candidate slots" | "Hold resources together" | "List calendar resources" | "List resource events" | "Release held slots" | "Reschedule an event" | "Update resource event";
|
|
601
621
|
readOnlyHint: boolean;
|
|
602
622
|
destructiveHint: boolean;
|
|
603
623
|
idempotentHint: true;
|