--- name: simulator-chat description: > Simulator.Company chat & messaging specialist — sending messages to a user, and creating or reusing p2p (1:1) and group chats. In Simulator a chat is an actor of the Events system form (data.chatType = "p2p" | "group"), its participants are the actor's access-rule members, and its messages are `comment` reactions. Use when the user wants to "message", "write to", "DM", "send a message to" someone, "open a chat with", "start a conversation", or "reply in the chat". Activate on "write a message to user N", "send N a message", "open a chat with", "message someone", "напиши повідомлення користувачу", "надішли повідомлення", "відкрий чат з", "почни розмову з", "напиши юзеру", "напиши сообщение пользователю", "отправь сообщение", "открой чат с", "напиши в чат". For comments/approvals on an arbitrary actor (not a chat) use `simulator-reactions`; for files use `simulator-attachments`; for sharing/access in general use `simulator-access`. --- > **Curated tool names (v2 server):** `searchUsers`, `getUsers`, `getSystemActor`, > `getForms`, `filterActors`, `createActor`, `getActorByRef`, `saveAccessRules`, > `createReaction`, `getReactions`. Call them by these exact names. There is **no** > dedicated "create chat" / "send message" tool — a chat is composed from these. # Simulator.Company Chat Specialist In Simulator.Company a **chat is an actor of the `Events` system form**: - `data.chatType = "p2p"` → a 1:1 chat · `"group"` → a group chat · empty → a plain event / SIP meeting (not a chat). - **Participants** = the chat actor's **access-rule members** (keyed by `userId`) — *not* a data field. The creator is the implicit owner; other participants are added with `saveAccessRules`. - **Messages** = **`comment` reactions** posted under the chat actor. There is no server-side "p2p has exactly two members / don't duplicate" enforcement — **this skill owns that discipline**, and it must match the platform's standard p2p convention or two clients will create duplicate chats for the same pair. The convention: - **Deterministic `ref`** = `p2pConversation__` — both participants' raw workspace userIds **sorted ascending**, joined with `_`. This is the p2p dedup key; the UI finds/reuses a chat *only* by this ref. - **`title`** = `:p2p:` — nicks ordered by the same ascending userId sort. - **`data`** = `{ chatType:"p2p", startDate:, endDate:, scheduleMeeting:false }`. - **Participants** are added by a **separate** `saveAccessRules` call (never inline in the create body); the creator is owner implicitly. > Read `$CLAUDE_PLUGIN_ROOT/docs/entities/chats.md` for the full model, the verbatim > Events-form field list, and the current-user constraint. Reply to the user in **their own language**. --- ## Test case: "write a message to user N" This is the canonical flow. Each step names the exact tool. ### 1 — Resolve user N ``` searchUsers(query="N") # accId defaults to the active workspace ``` Take N's `userId` from the result (e.g. `4210`). If several match, ask the user which one (or show name + email). `getUsers` lists everyone if you need to browse. > `getSystemActor(objType="user", objId=)` returns N's system "twin" actor. You > usually **do not need it** to chat — membership uses the raw `userId` — but it > get-or-creates the twin if some flow needs N represented as an actor. ### 2 — Resolve the Events form id (per workspace) ``` getForms(formTypes="system") # find the row whose title == "Events" → its id ``` The Events form id differs per workspace, so don't hardcode it. (You may instead pass `formName="Events"` to `createActor` and let it resolve the id.) ### 3 — Build the canonical ref + title (reuse key) The p2p dedup key needs **both** userIds. The recipient's came from step 1; resolve the **sender's** `userId` too (see the box below), then: ``` ids = sortAscending(senderUserId, N_userId) # numeric ascending ref = "p2pConversation_" + ids[0] + "_" + ids[1] # e.g. p2pConversation_4210_4310 title = nick(ids[0]) + ":p2p:" + nick(ids[1]) # e.g. olena:p2p:petro ``` > **Getting the sender's userId.** There is **no PAPI "current user" endpoint**, and the > auth token carries the caller's `saId`+`nick`, not the per-workspace `userId`. So: > ask the user, take it from context, or `getUsers` and match the token's `saId`/`nick` > to a member's `id`. **If you cannot get it,** fall back to discovery-by-member in > step 4 and skip the ref (note: a ref-less chat may be duplicated later by the web UI, > which keys reuse on ref). ### 4 — Find an existing chat (reuse, don't duplicate) Canonical — by the deterministic ref, exactly as the web UI does: ``` getActorByRef(formId=, ref=) # found → reuse its id, skip to step 6 ``` Fallback when the sender's userId is unknown — discover by participant (still finds UI-created chats, since the recipient is an access member): ``` filterActors(formId=, q="chatType=p2p", members=":view") ``` ### 5 — Create the p2p chat (only if none exists) ``` createActor( formName="Events", # or formId= title=, # "<nickA>:p2p:<nickB>" ref=<ref>, # p2pConversation_<minId>_<maxId> — REQUIRED for UI reuse data={ "chatType": "p2p", "startDate": <nowUnixSeconds>, "endDate": <nowUnixSeconds>, "scheduleMeeting": false }) # plain unix SECONDS on Events — not a calendar object ``` `startDate`/`endDate` are **required** by the Events form. On a chat they are just "now" as integer unix seconds (the nested calendar `{startDate,endDate,...}` object used elsewhere does **not** apply here). Then add N as the second participant (the creator is owner implicitly — do **not** add a self access-rule): ``` saveAccessRules( objType="actor", objId="<newChatId>", rules=[{ "action":"create", "data":{ "userId": <N_userId>, "privs":{ "view":true, "modify":true, "remove":true } } }]) ``` `saveAccessRules` is applied asynchronously and returns a `taskId`; posting the message next still works. ### 6 — Post the message (a `comment` reaction) ``` createReaction(type="comment", actorId="<chatId>", description="<the message text>") ``` That `comment` reaction under the chat actor **is** the chat message. To read the conversation back: `getReactions(actorId="<chatId>", view="flat", orderValue="ASC")`. --- ## Variations - **Group chat.** Create with `data.chatType="group"` and **no `ref`** (the UI gives group chats no dedup), then `saveAccessRules` (or `bulkSaveAccessRules`) for every participant `userId`. To find a group's members later, filter by them: `filterActors(members="11:view,22:view,33:view", q="chatType=group")`. - **Reply in a thread.** Pass `parentId=<reactionId>` to `createReaction` (see `simulator-reactions`). - **Attach a file to a message.** Upload first (`uploadBase64` → `attachId`), then `createReaction(..., attachments=[{attachId:<id>}])` — see `simulator-attachments`. - **Post quietly (no notification).** `createReaction(..., notify=false)`. - **Hand the message to the AI agent.** `createReaction(..., extra={mcp:true})` runs the platform AI agent on it under the requesting user's access (see `simulator-reactions`). ## Reuse & safety rules - **Always look up (step 4) before creating (step 5).** Creating a second p2p Events actor for the same pair silently produces a duplicate chat — the platform does not dedupe; only the deterministic `ref` does. - **Always set the canonical `ref` on create** (`p2pConversation_<minId>_<maxId>`) so the web UI reuses the same chat instead of making its own duplicate. - **Participants are access rules, never `data` fields.** Do not store member ids in `data`, and do not add a self access-rule (the creator is owner implicitly). - **Confirm before messaging on the user's behalf** if the message is outward-facing — a chat message notifies the recipient. - A chat needs `chatType` set; an Events actor without it is a plain event/meeting, and comments on it are ordinary discussion, not chat messages. --- ## Relationship to the other skills | Skill | Boundary | |---|---| | `simulator-agents` | Delegating work to a person as an agent (assess who fits, then act / task / message) — it composes this p2p flow for its "message the person" outcome. | | `simulator-reactions` | Generic comments/approvals/ratings on **any** actor (a chat message is a `comment` reaction — that skill covers the reaction mechanics). | | `simulator-actors` | The Events actor's own `data` fields and the actor `data` value protocol. | | `simulator-access` | Access rules in general (chat participants are access-rule members). | | `simulator-attachments` | Files on a message/actor. | | `simulator-init` | Login + workspace selection (needed before any of this). | ## Reference Documents | Path | When to read | |---|---| | `$CLAUDE_PLUGIN_ROOT/docs/entities/chats.md` | The chat model: Events form, chatType, members-as-access, messages-as-reactions | | `$CLAUDE_PLUGIN_ROOT/docs/entities/reactions.md` | Reaction types, tree/threading, AI-agent reactions | | `$CLAUDE_PLUGIN_ROOT/docs/entities/system-forms.md` | The Events system form among the built-ins | ## Tips - `accId`/workspace defaults to the active one — make sure `simulator-init` ran first. - The Events form id is **per workspace** — resolve it (`getForms`/`formName`), never hardcode. - On Events, `startDate`/`endDate` are plain unix **seconds**, both "now" for a chat. - Canonical reuse = `getActorByRef(ref="p2pConversation_<minId>_<maxId>")` (matches the web UI); it needs both userIds — the sender's isn't in the token (`saId`/`nick` only, no PAPI /me), so derive it or fall back to `filterActors(members=…, q="chatType=p2p")`. - A message is just `createReaction(type="comment", actorId=<chatId>, description=...)`.