Skip to Content
SimulatorsSlack changelog

Simulators

Slack changelog

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  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.

Last updated on