# Slack changelog

What changed in each Slack simulator release, newest first.

What changed in each Slack simulator release, newest first.

## v0.0.20 — 2026-11-04

1. **The state UI is included on Free.** `/_rystic/ui` is available on every plan, including Free. The Pro-only verified UI download is a separate feature.

2. **Setting the clock never stamps records before the timeline events it fired.** When `POST /_rystic/clock {"time": ...}` fires timeline events due at its target, the next request's records stamp after theirs instead of reusing their instant; a set past every event still stamps one millisecond past its target, as before.

3. **A paced clock keeps wall time, and `rystic run --speed 0` stops it.** Under `RYSTIC_PACE`, each tick moves the clock by the wall time that passed since the last, so a tick missed while the twin was busy no longer makes timeline events fire late. `rystic run` hands the twin only the pace it resolved, so `--speed 0`, a scenario without events, `--tape` and `--live` keep the clock manual even when your shell exports `RYSTIC_PACE`.

4. **Request-URL delivery retries as Slack does.** An event your app does not ack with a 2xx within 3 s is retried after 0 s, 1 min and 5 min, re-signed and numbered in `x-slack-retry-num` with `x-slack-retry-reason`, and a non-2xx answer carrying `x-slack-no-retry: 1` stops that event's retries.

5. **`rystic grid -evals` turns on Slack event push for an agent that asks for it.** The archive's manifest declares how: an agent with `events: socket` gets a twin started with `SLACK_TWIN_SOCKET_MODE=1`, and one with `events: webhook <port> <path>` gets `SLACK_TWIN_EVENTS_URL` pointed at `http://127.0.0.1:<port><path>`.

6. **Events pushed over Socket Mode carry the twin's URLs.** A URL on a Slack host inside an event pushed over Socket Mode, or read from `GET /_rystic/egress`, is on the twin's origin, as the Web API and request-URL delivery already answer it; `RYSTIC_PRESERVE_SERVICE_URLS=1` keeps Slack's hosts.

7. **Swagger UI to read and call the twin from the browser.** `/_rystic/swagger`, linked from the state UI header, is Swagger UI over the published API trimmed to the operations this twin serves; Try it out sends each request to the twin.

8. **Moving the clock past a request-URL retry sends it at once.** A request-URL retry still waits its 0 s, 1 min or 5 min on the wall clock, and `POST /_rystic/clock` past its due now sends it straight away; a retry waiting out its delay no longer refuses a new event as `queue_full`. The `delivery` report on `GET /_rystic/egress` adds `retries_armed`, `next_retry_at` and `next_retry_clock`.

9. **Requests stay fast as scheduled messages accumulate.** A request no longer rescans every queued scheduled message: the twin settles them only once a write lands or the next `post_at` comes due.

10. **The docs page lists the clients known to work.** A new known-clients table under Connect your client names each official client checked against this simulator, the release checked, the ways it connects that work and fail, the day it was checked, and any known gap that covers a failure.

11. **The @slack/web-api client's first call is checked on every round.** The `auth.test` call @slack/web-api makes before anything else is now a test measured against real Slack in every full round, so a change that breaks how the client starts shows up before release.

12. **A state dump seeds back as it was, and the next record gets a fresh id.** `GET /_rystic/inspect` posted to `POST /_rystic/seed` after a reset restores every record where it was, per-caller records included, which used to land under a doubled key. The next record created after any seed passes every uuid and event id the simulator minted that the seed holds. `GET /_rystic/inspect?view=sequences` lists every id counter, and a world document's new `sequences` key restores them, so ids the simulator renders from a counter continue where the dump left off.

13. **The verified UI shows the controls real Slack shows.** The rail carries Activity, Agents & tools, Admin, Create new and focus mode in Slack's order; the top bar carries Slackbot AI and Help; the channel header carries huddle, notifications, search in channel and a More actions menu (details, star, copy link, leave or close); the tabs carry Add canvas and +; the thread pane's header carries Summarize and a More menu with Copy link; the composer carries More options and the hover bar Forward. Each control works against the twin or renders disabled and says why on click. An avatar shows the person's photo when the twin serves one on its own origin, and `?theme=dark` opens Slack's dark theme.

14. **The verified Slack UI redraws after a reset.** An open page shows the twin's new state when the twin is reset or a scenario or seed document loads, with Socket Mode on or off, and forgets the drafts, saved items and sidebar state it kept from before, so an agent starting its next episode never sees the last one's state. No reload by hand.

15. **Each Socket Mode app gets its own copy of every event.** An app-level token shaped like Slack's, `xapp-1-<app id>-…`, connects as that app, whose envelopes carry its `api_app_id` and whose acks and redeliveries are its own; any other token connects as the default app, `AWDCBOT0001`, as before. Two bots, or a bot beside the verified UI, each receive every event instead of splitting them, and a bot's own connections still share its events without duplicates. A bot whose token carries a real app id now sees that id in `api_app_id`; set `SLACK_TWIN_APP_ID` to it to keep the default app.

16. **Every member in the shipped scenario has a profile picture.** In `design_studio`, `users.info`, `users.list` and `users.profile.get` answer each member's `image_24` through `image_512` as a default avatar on the simulator's own address, and the simulator serves each image, so a Slack client draws a picture instead of a blank tile.

17. **The verified UI has Threads and Activity.** The sidebar's Threads opens every thread you started or replied in, newest activity first, each with its latest replies and a reply box. The rail's Activity lists mentions of you, replies in your threads and reactions to your messages, filtered by kind, and each item opens its message. Both read the twin's public history and replies and update live.

18. **A DM's details name the other member, and say when you closed it.** `conversations.info` answers a DM's `user` as the other member from whichever side reads it, and after `conversations.close` answers `is_open: false` for you until `conversations.open` reopens it; the other member's view is unchanged.

19. **`chat.scheduledMessages.list` lists only your own scheduled messages.** Each token sees the messages it scheduled, never another member's, even in a channel you share. The `design_studio` scenario's queued messages each name who scheduled them.

20. **The verified UI hides a DM you closed.** Closing a direct message takes it out of the sidebar and the DMs list, and it stays out across reloads until you open it again or, for one you closed in this browser, the other person writes to you. The Scheduled tab no longer carries a notice that the twin lists everyone's scheduled messages.

21. **One container for the simulator and its verified UI.** The Slack Verified UI page builds a single image from the simulator's and the UI's release images, with one health check, and shows how to seed a workspace per task, reset it between episodes, read the end state for grading, which UI features work and what each environment costs to run.

22. **Search, reminders, user groups and custom emoji.** The twin serves `search.messages`, `search.files` and `search.all`; `reminders.add`, `reminders.list`, `reminders.info`, `reminders.complete` and `reminders.delete`; the seven `usergroups.*` methods; and `emoji.list`, each measured against live Slack. Search and reminders take a user (`xoxp-`) token, as Slack's do, and answer a bot token `not_allowed_token_type`.

23. **A person can press your bot's button or submit its modal.** A Slack client calls `blocks.actions` or `views.submit` with `X-Rystic-Surface: client`, and the app that owns the message or view receives Slack's `block_actions` or `view_submission` payload, over Socket Mode as an `interactive` envelope or as a signed `payload=` form post to `SLACK_TWIN_INTERACTIVITY_URL` (else the events URL), with a `trigger_id` and a `response_url` that replaces, deletes or answers the message. The client's call waits for your app's 3 s ack and hands back what it sent, such as a submit's `response_action`.

24. **The verified UI searches and shows user groups, custom emoji and reminders.** The top bar's search and the channel header's Search in channel open a results view with Messages and Files tabs, the query's words lit and each result opening its message or thread. Custom emoji from `emoji.list` fill the emoji picker's Custom section and draw in messages and reactions, aliases included. A user-group mention draws as its `@handle` and opens a card of its members. The composer's `@` offers groups. Directories gains a User Groups tab for creating, editing, disabling and enabling groups and for setting their members. A message's More actions offers Remind me. Later lists your reminders with Mark complete and Delete.

25. **The verified UI presses your bot's buttons.** A Block Kit button in a message, in an actions block or as a section's accessory, sends `blocks.actions` as the person viewing, after the button's confirm dialog when it has one, so your app receives `block_actions` and whatever it does to the message through `response_url` or `chat.update` shows as it happens. When your app is not connected, does not ack within 3 s or answers an error, the button shows Slack's generic error. Selects, overflow menus and date pickers stay disabled and say why. A modal your app opens does not show in the UI yet.

26. **A slow client no longer holds up the others.** The simulator reads a request's whole body before it starts serving it and sends the answer after, so a client that uploads slowly, or reads a large answer slowly, no longer delays other clients' requests. Answers are unchanged; one larger than 4 MiB still makes other requests wait while it is sent.

27. **Message search answers only from conversations you can read.** `search.messages` and `search.all` list a message only from a public channel, or from a private channel, DM or group DM you're in, as Slack does: another member's DM or a private channel you're not in no longer shows up, in the matches or the totals. A DM's match is typed `im` and named for the other member. File search (`search.files` and the file results of `search.all`) is unchanged.

28. **Search results link a reply into its thread and mark the words that matched.** A reply's `search.messages` permalink ends in `?thread_ts=` and its thread's timestamp, and `highlight=true` wraps each matched word in U+E000 and U+E001, as Slack does.

29. **Message search applies Slack's search modifiers.** `search.messages` and `search.all` narrow their message results by `in:` (a channel as `#name`, `name`, `<#C…>` or `<#C…|name>`, a DM as `<@U…>` or `@name`), `from:` (`<@U…>`, `@name`, `name` or `me`) and `has:link`, `has:pin` or `has:reaction`, as Slack does. They also narrow by `before:`, `after:`, `on:` and `during:` dates (`YYYY-MM-DD`, `YYYY/MM/DD`, `today`, `yesterday`, a month or a year). Words match whole and all must match. `"Phrases"` keep their order and `-` excludes. Two `in:` or `from:` terms match either. `highlight=true` marks only the words you searched. `has::emoji:` and the file results are not filtered yet.

30. **Canvases and Lists.** The twin serves `canvases.create`, `canvases.edit`, `canvases.delete`, `canvases.getContent`, `canvases.sections.lookup`, `canvases.access.set`, `canvases.access.delete` and `conversations.canvases.create`. It also serves the twelve `slackLists.*` methods for Lists, their items, access and CSV export. User groups now answer as a workspace that lets members manage them: `usergroups.create`, `usergroups.update` and `usergroups.users.update` succeed, and a group carries Slack's full set of fields.

31. **A request URL answers Slack's url_verification before its first event.** With `SLACK_TWIN_EVENTS_URL` set, the twin sends Slack's signed `url_verification` challenge first and holds events until your app answers HTTP 200 with the `challenge`, as text, JSON or a form, as Bolt does. `GET /_rystic/egress` says why events wait under `delivery.handshake`. A receiver that does not answer the challenge gets no events now: answer it, or set `SLACK_TWIN_URL_VERIFICATION=off`, which stands for a URL set by `apps.manifest.update`, to which Slack sends no challenge.

32. **Wait for your bot's turn instead of sleeping.** `GET /_rystic/settle` answers once no event or interaction awaits your bot's ack and no Web API call has landed for `quiet_ms` of wall time, or at `timeout_ms` with what is still open; calls made with `X-Rystic-Surface: client` never count, and `principal=` watches only your bot's. Its `last_call_seq` is the last `seq` in `/_rystic/calls`, so the calls between a settle before you act and one after are your bot's turn. `rystic settle` and the MCP server's `settle` tool ask the same route.

33. **The verified UI opens canvases and Lists.** Create new lists Slack's entries and makes a canvas or a List, and the rail's Files gains Canvases and Lists pages. A canvas shows its headings, lists, checklists, quotes, tables and code beside an outline of its headings; you add to it at its foot, edit or delete any part of it and tick its checklist items. A channel's Add canvas tab makes the channel canvas and opens it in place, and the tab strip's + menu shows Slack's entries, with Link adding a bookmark. A List shows as a table with select pills, people, dates, checkboxes and a to-do List's completion circles, and an Item panel beside it holds every field, Mark Complete and the subtasks. You add items and subtasks there, edit cells, delete one item or several, search the List and rename it. A List a channel names among its tabs opens as a tab. The Canvases and Lists pages, a canvas's title, a channel canvas made elsewhere and Download as CSV show what the twin can't answer yet.

34. **The call log shows what each call asked for and whether Slack served it.** Each `GET /_rystic/calls` entry now carries `args`, the call's arguments with tokens redacted (long values cut, file contents left out), and `ok`. A refusal Slack answers with HTTP 200 has `ok: false` and its code in `error`, `unknown_method` included, and a method the simulator does not have is also marked `unknown_route: true`. `?tail=N` returns only the last N calls, and `dropped` counts the calls the log no longer keeps.

35. **Your bot sees a member leave.** Deactivate a member with a seed or a scenario timeline `seed` event setting `deleted: true`, and every app receives one `user_change` with the whole user, as on Slack; a reactivation, or a seed that changes another field of the user object, sends one too. The deactivated member leaves every public and private channel but keeps its DMs and stays in `users.list` with `deleted: true`, and `conversations.members` excludes it.

36. **A scheduled message that falls due reaches your app, and a person's posts as the person.** When the clock reaches a `chat.scheduleMessage` message's `post_at`, the twin sends the `message` event, and `app_mention` when it mentions a bot user, over Socket Mode or your request URL, as `chat.postMessage` does. A message a person scheduled with `X-Rystic-Surface: client` posts as they typed it, with its `client_msg_id` and no `bot_id`, `app_id` or `bot_profile`, in its preview, in history and in its events.

37. **The verified Slack UI stays signed in through a reset or a restart.** After a reset, a scenario or seed load, a credential binding or a simulator restart, the UI binds every person's token again before an open page redraws, so a post from the page lands as its person, never as the bot, and a harness no longer runs `setup-twin.mjs` after each reset. It binds with the new `"keep": true` on `POST /_rystic/credentials`, which adds only the credentials not bound yet, so a harness's or scenario file's own bindings stay.

38. **The docs are one page, with a page per feature.** The archive ships `README.md` in place of `HOWTO.md`, with the same sections in the same order as every Rystic simulator's page, and `CHANGELOG.md` beside it. `SCENARIOS.md` no longer ships; the Scenarios section of the page lists each scenario. Identities, the Events API, interactivity, search, the MCP server, the workspace UI and the verified UI (with its one-container environment) each have their own page under `docs/features/`, and every example on these pages runs as written against a fresh simulator.

39. **The docs page links the admin token section.** The page now sends you to the admin token's section of the shared-simulators page.

## v0.0.19 — 2026-10-12

1. **A world can carry its own timeline.** A `--seed` file, or a complete world document posted to `POST /_rystic/seed`, may add `credentials`, binding presented credentials to actors, and `events`: calls the twin makes on its own API at set sim times as one of those actors, through the same handlers and egress as your requests. A top-level `credentials` or `events` key the twin used to ignore now takes effect. `RYSTIC_PACE=<speed>` plays the clock, and the timeline, in real time, and an event's `at` may be `+<duration>`. `/_rystic/inject`, `--inject` and `RYSTIC_INJECT` now need `state_set` instead of `scenarios`.

2. **A timeline call can hand what it minted to later events.** A `call` event may add `bind`, `{"<name>": "<JSON Pointer>"}`, saving fields of its answer, such as an ID the twin minted; a later event uses one as `${name}` in a call's path, headers or body or in a seed event's record keys and values (a register name cannot be bound). A schedule naming a `${name}` no earlier call binds is refused at load, and an event whose binding call failed records an error instead of firing.

3. **A clock jump fires each timeline event at its own time.** When one `POST /_rystic/clock` or paced tick passes several events, each fires with the clock at its `at`, so the records it creates carry that time, and the clock then lands where the move asked. An event already due when the clock moves fires at the current time.

4. **A clock jump stamps each timeline event at its exact instant.** When a clock move passes timeline events, the records each one makes carry exactly its `at`, to the millisecond, whatever traffic came before it, so one jump leaves the same records as stepping the clock to each event in turn.

5. **A timeline call can carry raw bytes, XML or a JSON array.** A `call` event may send `body_text` or `body_base64` instead of `body`; the twin sends it as written, under the route's own content type (XML, bytes, JSON or a form), so a scheduled call can upload an object's bytes or post an XML document, and `${name}` fills in `body_text`. A `body` may be a JSON array where the route reads one. A call that answers XML binds from its elements by JSON Pointer, such as `/InitiateMultipartUploadResult/UploadId`.

## v0.0.18 — 2026-10-11

1. **The verified UI installs and starts with `rystic ui slack`.** The Slack client built against Slack's own client is a Pro and Enterprise deliverable: it ships as the image `registry.rystic.ai/slack-twin-ui:<ver>` beside the twin's, pulled with the same `docker login`, and `rystic ui` runs it against a running instance (or, without Docker, the UI's archive on your Node 22+). A Free key is refused with `feature_not_in_plan`, naming the `verified_ui` entitlement; the Web API and the embedded state UI stay on every plan.

2. **The scripts in this archive are MIT-licensed.** `check.sh` and the archive's other scripts are published under the MIT License at https://github.com/Rystic-Inc/rystic-scripts, so you can copy, change and share them. `LICENSE.txt` no longer covers them; it still governs the twin itself.

## v0.0.17 — 2026-10-10

1. **Scheduled messages post when the twin's clock reaches them.** A `chat.scheduleMessage` posts once, as its author and in its thread, when simulator time reaches `post_at`, and then leaves `chat.scheduledMessages.list`; a canceled message never posts.

2. **Ephemeral posts reach the activity stream, and stream starts keep their chunks.** A successful `chat.postEphemeral` emits one `slack.message.ephemeral` event on `/_rystic/events` naming its recipient, and never enters history. A `chat.startStream` with `chunks` and no `markdown_text` stores the chunks' text as the message text.

3. **Closing a DM closes it only for you.** `conversations.close` closes the DM for the caller alone and leaves the other participants' view untouched; `conversations.open` on it reopens it for the caller with their read position kept.

4. **Deleting a message removes its pin and its reactions, as Slack does.** After `chat.delete`, `pins.list` on the channel no longer lists the message, and `reactions.get` on it answers `message_not_found`. A thread parent kept as a tombstone, because it still has replies, keeps both; that case is not yet measured. The pin behavior was measured against a live workspace.

5. **Conversation answers name your workspace's team, and list and info agree.** `conversations.create`, `invite`, `join`, `rename`, `setTopic` and `setPurpose` answer `shared_team_ids` with your workspace's team and leave out the `latest` and unread counts Slack omits there; `conversations.list` and `conversations.info` give one conversation the same `creator` and `updated`, whoever asks, and a new group DM is named `mpdm-…` from its members' handles. `setPurpose`, `rename` and `join` leave their system message in channel history, and a streamed message carries your bot's `bot_id`, `app_id` and `bot_profile`, reads back as `in_progress` with the text sent so far until `chat.stopStream`, and finishes as `completed`.

6. **A deleted file leaves a tombstone on its share, and a file names its uploader's team.** After `files.delete`, a message that shared the file keeps it as `{"id": …, "mode": "tombstone"}`, as Slack does, and `files.info` answers the uploader's team in `user_team` instead of a placeholder.

7. **Refusals carry Slack's diagnostics, and a published view names your app.** Every `invalid_arguments` refusal carries `response_metadata.messages`, `conversations.invite` refusals carry the per-user `errors` array, and `views.publish` answers your app's `app_id` and `bot_id` rather than fixed placeholders. A `post_at` that is not a timestamp on `chat.scheduleMessage`, a presence other than `auto` or `away` on `users.setPresence`, and `markdown_text` beside `chunks` on `chat.stopStream` answer `invalid_arguments`, as Slack does. `conversations.invite` with `force=true` passes over an unknown user only while another named user can still be added, so beside members alone it answers `user_not_found`.

8. **Answers carry what Slack's documented responses fix.** Where a handler leaves a field absent or null, the twin answers with the constant, the documented default or the `ok` tag the method's documented response fixes for that status, error envelopes included; a value the handler wrote is never replaced.

9. **Tokens are read and refused the way Slack reads them.** A token that is not a Slack token (`Bearer x`, `Bearer token`) now answers `invalid_auth` instead of being accepted. A malformed `Authorization` header — `Bearer` alone, or a second one — answers `not_authed`, as live Slack answers `Bearer` alone on every method but `api.test`, which it answers `ok`, though live Slack refuses a second header with `invalid_auth`; a header under another scheme, such as `Basic`, presents no token and answers `not_authed` too. A form-encoded `token` now wins over the header, and of two form tokens the last, as live Slack answers; `api.test` still answers without a token but refuses an invalid one. Rotated `xoxe.xoxb-`/`xoxe.xoxp-`, app-level `xapp-` and workflow `xwfp-` tokens are recognized kinds, so an app-level token on `users.identity` answers `not_allowed_token_type`.

10. **A write moves the records it relates to, as Slack does.** A stream started in a thread (`chat.startStream` with `thread_ts`), or a file shared into one (`files.completeUploadExternal` with `thread_ts`, naming its one conversation in `channel_id` or `channels`), moves its parent's `reply_count`, `reply_users`, `reply_users_count` and `latest_reply`, as a posted reply does, and the file's share message is a reply in that thread. `pins.remove` takes the channel off the message's `pinned_to`, so `conversations.history` counts one pin fewer. `conversations.info` answers your `last_read`, which `conversations.mark` moves.

11. **An empty workspace holds the bot every token acts as.** A twin started without a scenario, and the workspace `POST /_rystic/reset` wipes to, now hold one bot user, `tb-member-user-id` of team `tb-member-team-id`, and every token acts as it. A method that names it now finds it: `users.list` lists it, and `users.info` and `users.profile.get`, among others, answer it instead of `user_not_found`. A user token's `users.identity` answers it instead of `invalid_auth`, and what you create is authored by it: a channel's `creator`, and a message's `user`, `team`, `bot_id` and `app_id`, where a minted `U00000001` of team `T00000002` authored them before. The workspace still holds no team record, so `auth.test` answers `invalid_auth` as before. A seed that keys its own caller under `member` replaces this bot; one that keys every user by its `id` keeps it beside them, and every token still acts as it.

12. **HOWTO.md counts every route the twin answers.** Its coverage sentence reads 274 routes where it read 73: each of the 67 modeled methods counts at `GET` and `POST` on both `/<method>` and `/api/<method>`, as `GET /_rystic/routes` lists them. The twin answers what it did before.

13. **A file is shared only once its bytes are uploaded, and a /me message reads back as Slack's.** `files.completeUploadExternal` on a file whose bytes never reached its `upload_url` still answers `ok`, but leaves the file untyped and unshared, posts no message and moves no thread parent, as Slack does: upload the bytes first to share a file. A `chat.meMessage` message reads back with your app's `app_id`, a `rich_text` block and no `team`, and `reactions.get` answers a message's `pinned_to` while it is pinned.

14. **A first close answers its flags, an in-flight stream reads back empty, a thread parent carries its thread_ts, a broadcast reply and a /me edit answer no bot_profile, and an IM carries is_archived.** `conversations.close` answers the first close of an open DM with `no_op: false` and `already_closed: false` beside `ok`. `reactions.get` on a stream still in flight answers an empty `text` with `streaming_state: in_progress`, the bot's `bot_id` and `app_id`, and `parent_user_id` on a reply. A thread's parent answers `thread_ts` equal to its own `ts` once any reply joins it, a streamed reply included. `conversations.history` lists a broadcast reply (`reply_broadcast`) with no `bot_profile` or `team`, and `chat.update` of a `/me` message answers neither. An IM's `conversations.info` carries `is_archived`, and its `latest` bot message's `bot_profile` carries a `name`.

## v0.0.16 — 2026-10-02

1. **Socket Mode envelope IDs come from their own range.** The first envelope the twin sends carries `envelope_id` `cdf8b689-d7dc-46e6-8000-000000000001` rather than `00000001-0000-4000-8001-000000000001`, and later ones count up in their last digits. A client that acknowledges the ID it receives sees no difference; a test that pinned an envelope ID needs the new one.

2. **Post as a person with `X-Rystic-Surface: client`.** A `chat.postMessage` made as a seeded non-bot user with the header `X-Rystic-Surface: client` stores and emits the message that user would type in the Slack client: `user`, `team`, `text`, `blocks` and a `client_msg_id`, with no `bot_id`, `app_id` or `bot_profile`, so an app that ignores bot messages starts a turn on its `message` and `app_mention` events. Posts without the header keep their app attribution, as on Slack.

## v0.0.15 — 2026-10-01

1. **`check.sh` fails fast on a port that never answers.** Its port check and boot wait give each `/_rystic/version` probe two seconds, so an address where something accepts connections but never replies fails the check with a message instead of hanging.

2. **Your bot receives Events API events, over Socket Mode or at a request URL.** Run the twin with `SLACK_TWIN_SOCKET_MODE=1` and point Bolt at it with any `xapp-` token, or set `SLACK_TWIN_EVENTS_URL` and `SLACK_TWIN_SIGNING_SECRET` to have each event POSTed signed. Messages, edits, deletes, mentions, reactions, pins, file shares and channel joins, leaves, creates, renames and archives emit the events Slack sends for them.

## v0.0.14 — 2026-09-30

1. **Inspect offers an identity view, and an injected 429 can carry `Retry-After`.** `/_rystic/inspect?view=identities` folds identical actor-label aliases for display; the default export retains all records so a reset followed by seeding restores caller identity. A network profile's `reject` takes validated `headers`, so an injected `429` can send the `Retry-After` a client's backoff reads. The docs now say what file downloads answer, how `state_reaction` is keyed, and that a deleted file's share message keeps its full file.

2. **File URLs serve the bytes.** A `GET` of a file's `url_private` or `url_private_download` with a Bearer token returns the bytes you uploaded, under the file's `mimetype` (the download form as an attachment); without a token it answers `401`, and a file whose bytes never reached the twin answers `404`. `files.completeUploadExternal` now returns the file's `mimetype`, `mode`, `pretty_type`, `is_public` and `url_private_download`, as Slack does.

3. **Thread identities and query errors.** Thread replies preserve an existing bot ID from the caller. Missing-argument errors from permalink, file-info, and email-lookup reads, and invalid-limit errors from reactions-list and user-conversations reads, include populated response diagnostics; replies and file-info reads reject non-integer `limit` values.

4. **The state UI shows the workspace as a Slack client.** On Pro and Enterprise, `/_rystic/ui` opens a live Slack-style view of the twin — channels, threads, DMs, Block Kit, App Home and modals — where you can view as any seeded user and post, reply, react, pin, edit or join as them through the Web API, beside Your Bot, Scheduled messages and Views cards.

5. **The state UI takes the rystic.ai look.** `/_rystic/ui` and its Your Bot, Scheduled messages and Views cards now read in the site's paper, ink and Space Grotesk instead of the dark theme; the workspace card keeps Slack's own look.

6. **Unmodified clients reach the twin at Slack's hostnames.** `rystic run slack-twin --intercept` boots the twin behind the rystic CLI's HTTPS gateway, and `rystic exec -- <command>` routes that command's calls to `slack.com` and `slack-files.com` to it, for supported SDKs using their default transports.

7. **Missing and invalid arguments answer Slack's errors.** A call missing a required argument answers `invalid_arguments` with Slack's `response_metadata.messages` (`[ERROR] missing required field: channel`), except `users.info` without `user`, which answers `user_not_found` as Slack does. `chat.postMessage` with no text, blocks or attachments answers `no_text`, `reactions.add` with an emoji name the workspace doesn't know answers `invalid_name`, and `users.getPresence` without `user` reports the caller's own presence.

8. **Thread and message reads reflect earlier writes.** A `reply_broadcast` reply appears in channel history as a `thread_broadcast` carrying its parent as `root`. Deleting a reply updates its parent's `reply_count`, `reply_users` and `latest_reply`; deleting a parent that has replies leaves a tombstone ("This message was deleted.", from `USLACKBOT`) that keeps its thread, and deleting it again answers `message_not_found`. An updated message carries no `parse` or `link_names`, and a post answers no `username` unless you sent one.

9. **Pins list newest first, and Slackbot is a listed user.** `pins.list` lists the most recently pinned item first, pins made in the same second included, and each pinned message carries `pinned_info`. `users.list` lists the built-in Slackbot first, as the same object `users.info` returns for `USLACKBOT` (`is_bot` false, Slack's avatars). Lists the twin doesn't otherwise order come back in the order their records were created, the same on every run.

10. **History windows and reply reads follow Slack.** `conversations.history` with `oldest` returns the messages just after it, not the channel's newest, and its cursor pages forward from there, so a bot that catches up from a checkpoint sees every message once. `conversations.replies` with a reply's `ts` returns that reply alone, not its whole thread.

11. **Long messages are truncated at 4,000 characters.** A message longer than 4,000 characters is stored, and read back from history, as its first 4,000 characters; the post's own answer still echoes the full text, as Slack's does.

## v0.0.13 — 2026-09-27

1. **Several agents can share one workspace, each under its own token.** `POST /_rystic/credentials` binds a token to a user, and every call with that token acts as that user, with no Rystic header. `GET /_rystic/calls` lists every request since reset with the identity that made it, for grading. See Identities in the Slack docs.

2. **Conversation access follows each identity.** Private conversation reads respect current membership, including after leaving; human user tokens can read public history without joining. Opening a human–bot DM from either identity reuses the same conversation, including an existing seeded DM.

3. **Private conversations stay hidden from non-members everywhere.** `conversations.list` shows a private channel, DM or group DM only to its members, as on Slack, and a non-member posting into one gets `channel_not_found`, the same answer reads give. A refused post no longer writes bot IDs onto the caller.

## v0.0.12 — 2026-09-26

1. **HOWTO.md and `check.sh` start from an API key.** HOWTO.md leads with `rystic login rys_…`, says which control-plane routes Free, Pro and Enterprise reach and what `POST /_rystic/reset` does on each, documents seeding (twelve registers, `state_trigger` included, and an inspect dump posted back as it is) and the token rules as served (`xoxb-`/`xoxp-` kinds, `not_allowed_token_type`, the form-body `token=` fallback), and states that the bundled MCP package, rebound to `design_studio`, is not yet re-qualified. SCENARIOS.md names `design_studio` as the world Free boots. `check.sh` runs the Free and Pro builds on your API key (`RYSTIC_TOKEN`, or the key `rystic login` stored) as well as the Enterprise build on its license file, looked up in the CLI's order.

## v0.0.11 — 2026-09-26

1. **Only `design_studio` ships as a named scenario.** The older scenarios are now test-only fixtures and are no longer accepted by `--scenario`, `RYSTIC_SCENARIO`, or `POST /_rystic/scenario`. Tests and evaluations can load their unchanged register maps and clocks from `sim/testdata/scenarios/` through the control plane.

## v0.0.10 — 2026-09-25

1. **Self-serve on the metered edition replaces the free edition.** A Rystic account's token runs the metered build: `rystic login rys_…` once, then `rystic run` or `docker run -e RYSTIC_TOKEN=…`. It carries every scenario and control-plane route your plan includes, and bills active 30-second blocks against the plan's allowance. The fixed-world free download and its usage report are gone.

2. **`design_studio` is the same studio on every twin.** Its people, clients and project dates now come from one shared world, so the clients, invoice numbers and dates `#finance` names match what the Stripe, PayPal, Resend and Lob twins hold for Website Design Contractors. The archived project channels were regenerated with each job's real page count: the workspace now carries 2,951 messages (was 2,959).

## v0.0.9 — 2026-09-22

1. **No change to the twin's behavior.** Same control plane, scenario pack,
   seeds, worlds and license gate as v0.0.8, and the free edition's
   `design_studio` world is unchanged. The bundled MCP package pins the same
   korotovsky/slack-mcp-server v1.3.0 and the same thirteen cases; each
   receipt its workflow checker writes now carries a `schema_version`.

## v0.0.8 — 2026-09-22

1. **A fourth world, `design_studio`: somebody else's whole Slack.**
   `./slack-twin --scenario design_studio` boots into a web design studio a
   year in — its own team (`TWDC0000001`), its own cast of forty-three people
   and bots, and its own bot token, `studiobot`. Sixty conversations: five
   company channels, five craft channels, ten live client projects, two Slack
   Connect channels shared with clients, four private channels, five DMs, a
   group DM and twenty-eight archived projects, carrying 2,959 messages with
   the pins, bookmarks, files, threads, reactions and queued messages a year
   leaves behind. Six questions are waiting in it that only a reader of this
   workspace can answer — who is on call, when a client launches, how a site
   is rolled back — and every one is answered by a record inside the binary.
   The three `Rystic Shop` worlds are unchanged, message for message, so a
   suite pinned to their `ts` values keeps passing. `SCENARIOS.md` has the
   workspace's channels, people and the six questions.

## v0.0.7 — 2026-09-21

1. **No change to the licensed twin's behavior.** Same control plane,
   scenario pack, seeds, worlds and license gate as v0.0.6.
   `/_rystic/version` gains an `edition:` line — empty on a licensed build —
   so a caller can tell a licensed twin from the free edition without
   inspecting anything else.

## v0.0.6 — 2026-09-21

1. **A pinned native MCP integration.** The release includes the
   korotovsky/slack-mcp-server v1.3.0 bot-token profile, native caller,
   bounded local gateway, workflow checker and exact thirteen-case pack.
   The declared tools discover channels, read history and threads, and post
   replies. Package manifests pin the binaries, tool manifest, model and
   shipped fixture files. See the bundled MCP README for configuration and
   explicit exclusions, including hosted Slack MCP and search.

2. **Protected runtime contract 9.** A trusted harness can require a private
   gateway credential on all twin requests and preserve service URL identities
   while routing the native caller locally. Service credentials supplied in
   accepted form bodies follow the same modeled authentication rules.

3. **Measured thread and message fixes.** Posting updates stored parent thread
   registers; history and replies preserve those values. Repeated-parent
   cursor pages, public block metadata and the single-section Markdown
   fallback used by the pinned caller follow the controlled workflow evidence.
   The API test response no longer echoes the bearer token.

**Measured scope.** The native local workflow passes 13/13 cases, a fresh
read-only comparison passes 8/8, and the Linux caller cell passes seven
containment checks. The original controlled live comparison and exact local
replay retain their separate model pins. These are scoped MCP results, not a
new full-surface Slack fidelity score. Released Harbor reset/isolation and a
real-agent rollout remain separate qualification gates; packaging alone does
not certify them.

**Qualification addendum, 2026-09-21.** The published 0.0.6 package now passes
13/13 shipped cases without a compiler in the offline Linux amd64 Harbor image.
Two-episode isolation, three repeated resets, in-flight cancellation, native
crash, gateway timeout and cleanup pass. The unchanged held-out task's MCP
reference earns 1.0; null, wrong-thread, duplicate-reply and forbidden-edit
controls fail their intended checks. The
[support card and dated receipts](https://github.com/Rystic-Inc/twin-envs/blob/main/docs/slack-mcp-support.md)
identify exact artifacts and limitations. One real held-out Opus rollout earns
1.0 with 17 MCP replies under the original grader. The source workflow ended red
because its checker compared a short row ID with Harbor's qualified name; the
retained original trace passes the corrected exact-name check. Both receipts and
source hashes are retained. No learning/generalization claim is made, and the
release archives remain unchanged.

## v0.0.3 — 2026-09-19

1. **A third world, `refund_desk_xl`.** `./slack-twin --scenario
   refund_desk_xl` boots into the same `#refund-desk` (`CRYSOPS0003`) over
   a busier week: twelve users, 45 top-level messages and five threads
   across five days, and seventeen open requests naming fourteen objects in
   stripe-twin's `busy_account` — the nine `refund_desk` names plus the
   paused v1 webhook endpoint, a completed and an expired checkout session,
   an unpaid cart and the production endpoint. The shapes an ops bot has to
   leave alone are all on the record: a status question already answered in
   its thread, the bot's own earlier summary naming a charge, a refund word
   with no payment ID behind it, the same customer asked about twice, and
   two objects in one message. `refund_desk` and `ops_channel` are
   unchanged, message for message, so a suite pinned to their `ts` values
   keeps passing. `SCENARIOS.md` lists the new world's requests and what
   each resolves to on the Stripe twin.

## v0.0.2 — 2026-09-18

1. **Two named worlds, baked in.** `./slack-twin --scenario refund_desk`
   boots into `#refund-desk`: one workspace, nine users, and a channel where
   eight people have left 19 messages — chatter, Stripe status questions and
   five refund requests, every payment ID a real record in stripe-twin's
   `busy_account` world — with the refund handbook pinned and one request
   already answered in its thread. `ops_channel` is `#ops`: the same people,
   14 messages of chatter and status questions and not one refund request,
   several of the questions answerable only from another twin. Your `xoxb-`
   token is the workspace's ops bot (`auth.test` → `UBOTOPS0001`) and already
   a member, so `conversations.history` answers without a join. Slack has no
   seed API and a bot cannot post as somebody else, so a channel with a
   history other people wrote is exactly the state a bot token cannot reach
   on its own. `--list-scenarios` names them; `RYSTIC_SCENARIO=<name>` does
   the same in a container.

2. **`SCENARIOS.md`.** One page in the archive naming each world, its
   channel and cast, what every request points at on the Stripe twin, and
   how to re-pin the handbook with a different refund window.

3. **`conversations.history` lists top-level messages only.** Thread replies
   no longer appear in a channel's history; they live under
   `conversations.replies`, as on Slack, with `thread_broadcast` replies the
   one exception. A bot's own posts now carry the `bot_id` and `app_id` that
   `auth.test` reports for its token, and each user's `tz_label` /
   `tz_offset` follow their `tz`.

## v0.0.1 — 2026-09-17

The first release of the Slack twin — an early cut, shipped so integrations
can start against it while fidelity work continues. Read the measurement
paragraph below before relying on a method's exact response shape.

1. **A local Slack Web API for your bot's integration tests.** 67 methods in
   the bot dialect across 11 resources: `conversations.*` (create, join,
   invite, kick, leave, archive, rename, topic and purpose, history, replies,
   members, mark, open, close, info, list), `chat.*` (postMessage, update,
   delete, postEphemeral, meMessage, getPermalink, scheduled messages, and
   the startStream/appendStream/stopStream trio), `users.*`, `reactions.*`,
   `pins.*`, `bookmarks.*`, the external file upload pair with
   `files.info/list/delete`, `views.open/publish/update`, `auth.test`,
   `api.test`, `team.info`, `team.profile.get`, `dnd.info` and
   `dnd.teamInfo`. Point your Slack SDK's base URL at the twin and your
   existing suite runs against it unchanged — form-encoded and JSON bodies
   both work, as they do live.

2. **Slack's error envelope, faithfully.** Slack answers failures as HTTP
   200 with `{"ok": false, "error": "channel_not_found"}`, so a client
   branches on the error string and never the status code. The twin does the
   same for every modeled method — a missing channel, a missing token
   (`not_authed`), a user-token-only method called with a bot token — so
   your error handling is exercised against the shape it will meet live.

3. **It never reaches the network, and it needs no app.** Any well-formed
   bot token (`xoxb-…`) is accepted and the twin dials nothing — which is
   the point: a suite that hits `slack.com/api` fails when a channel it
   created last week cannot be deleted (Slack has no channel delete), and
   one that posts to a shared test workspace fails when a colleague's run
   posts first.

4. **Seed the workspace your test needs.** `POST /_rystic/seed` takes a
   register map — channels, memberships, messages, users, the team,
   reactions, pins, bookmarks, files, scheduled messages, views — and lands
   it directly. `GET /_rystic/registers` lists what this twin accepts a seed
   for; a register the model does not carry is refused with a status rather
   than silently dropped.

5. **The same seed twice is the same workspace twice.** `POST /_rystic/reset`
   empties the twin, `POST /_rystic/seed-rng` pins the generator so channel
   IDs repeat run to run, and `POST /_rystic/clock` fixes the twin's notion
   of now — which matters more here than elsewhere, because a message's `ts`
   is both its ID and its clock reading. Two identical seed flows leave
   byte-identical `/_rystic/inspect` dumps.

**What is measured, and what is not.** Fidelity is scored differentially
against real `slack.com/api` on a throwaway workspace, per response field,
not by eyeball: the last full-suite round was **161 of 221 probes on
2026-09-17**. That is an early number and this release ships it as-is. The
misses cluster where the model is known to be thin: response fields the
Slack SDK samples list but a live response omits, cursor and `ts` values
that are volatile by design, and the reads whose live evidence came from a
newly seeded workspace. `HOWTO.md` lists the known shortfalls under
Limitations. Rounds are re-run as the model is repaired and each release
states its own number.

**Not in this release.** The Events API and Socket Mode, `admin.*`
(Enterprise Grid), Slack Connect and shared channels, user tokens (`xoxp-`)
and the methods only they can succeed at — `users.profile.set`,
`users.setPhoto`, `users.deletePhoto`, `users.identity` — which answer their
documented `ok:false` branch, as they do live under a bot token.

---

Source: https://www.rystic.ai/docs/simulators/slack/changelog · Markdown: https://www.rystic.ai/docs/simulators/slack/changelog.md
