# Copilot

> The operator's own assistant, on a page of its own — what it can read, what it may move, and the approval gate in front of everything else.

Page: https://sdk.chatfuel.com/docs/modules/copilot
Markdown: https://sdk.chatfuel.com/docs/modules/copilot.md

## What it is [#what-it-is]

The assistant is a page you go to, not a panel that follows you. There **was** a dock — the
thread rendered beside every other module, with a button in the top bar — and it was removed:
it put one module's chrome into every screen in the product. The module's own manifest and the
repo's module table still describe that dock; both are stale, and nothing in the app draws one.

What survived the removal is the part that was never about the panel. Because the page runs
inside the shell, the assistant can answer "what is the operator looking at" with the actual
screen, and it can move the app — and both work from its own page. Everything that would change
account data stops at a manual approval batch on the server first.

## Installed as [#installed-as]

|             |                                           |
| ----------- | ----------------------------------------- |
| Wizard id   | `coworker`                                |
| Install it  | `npx @chatfuel/wizard --modules coworker` |
| Selected by | Default — `--yes` installs it             |
| Requires    | Nothing, beyond the implicit `core`       |
| Recommends  | `livechat`                                |
| Skill       | `chatfuel-coworker`                       |

The module is neither `hidden` nor `railHidden`: it takes an ordinary rail item, in the Growth
group beside Publishing and Ads Optimization.

## Routes and views [#routes-and-views]

One path, one parameter. There is no view segment — everything the module shows is one thread.

| Path        | What it shows                                                                        |
| ----------- | ------------------------------------------------------------------------------------ |
| `/coworker` | The conversation list on the left, the open thread beside it, the composer under it. |

| Parameter | What it does                                                                          |
| --------- | ------------------------------------------------------------------------------------- |
| `c`       | The conversation on screen. Absent means the newest, or an empty page on a fresh bot. |

That one parameter still goes through a codec, and the surface reads it on **every** render
rather than seeding state at mount. The assistant's own `navigate` writes the URL, and a
mount-seeded surface would ignore it in silence.

## The model [#the-model]

**It reads the screen by asking, and it blocks while it waits.** The server sends
`CoworkerFrontendStateRequested` and waits about ten seconds for
`coworkerConversationFrontendStateSubmitReply`. Answer from something already alive: on a cold
page whose first module was still loading, the reply missed the window and the tool came back
`{"success":false,…}`. The reply is a `Map!` and not a string map — nested objects, arrays,
numbers and booleans all reach the model verbatim, so send structure rather than flattening it.
Exactly one client may answer a given `requestID`; a second one gets
`FrontendStateRequestNotFound`.

**It navigates by page name, never by URL.** `CoworkerFrontendAction {actionType: 'navigate',
parameters: {pathKey: 'Deals'}}` is a *named destination*. Asked which names it accepts, the
assistant answered with Chatfuel's own page names — Live Chat, Contacts, Leads, Calendar, Flows,
Billing, channel settings, automations, catalog, FAQ, API, teammates — several of which this
shell calls something else and several of which it does not have. So `lib/shellBridge.ts`
resolves a name against the module registry (title first, then id) with a sixteen-entry alias
table behind it, and builds the URL itself. A name that resolves to nothing is not an error; it
is a page this product does not include, and the thread says so.

A route change is reversible, which is why executing one needs no approval and why `undo` is
always offered. What guards it instead is *when*: an action runs only if it arrived on the live
socket, in the conversation on screen, has not run before, is not inside a two-second cooldown,
and is not landing while somebody is typing — unless they asked in the last two minutes, which
is the case the typing guard was built to allow. Every refusal shows a label instead of a
silent no-op: `Already done`, `Was opened earlier`, `Waiting for you to open this chat`, `Ready
when you are`. The history rule is the one that ships as a bug if it is an afterthought: a
frontend action is a *message*, so every page of history contains every navigation the
assistant ever performed.

**Every tool call costs two messages, and both have empty content.** The request carries
`toolCalls: []`; the result carries one entry. Failed results and rejections arrive as fully
empty messages — no content, no tool calls. Filtering on "has no content" therefore hides the
whole run. The rule that works is the one the module ships: &#x2A;no content and no tool calls is
noise; a message carrying tool calls is a step.*

Approval is conversation state rather than a message. `pendingAction` holds the whole batch,
**one boolean resolves all of it**, and none of it runs until you answer. Reads never reach the
gate — `chatfuel_gql-list_catalog` and `chatfuel_gql-list_specialists` ran with no
`pendingAction` at all. There is no expiry: a pending batch blocks the conversation until
somebody decides. Sending an ordinary message while one is pending *is* a rejection, with that
text as the denial message.

The module classifies each tool by its leading verb — fifteen read verbs, eleven destroy verbs,
everything else a write — and an unrecognised verb or an unknown tool id counts as a write.
That is the safe direction to be wrong in.

<Callout type="warn">
  The agent loop is rate-limited to roughly 100 starts a minute per user account, and going over
  it **fails silently**: the mutation already returned success and no reply ever arrives.
  `isAgentLoopActive` is Redis-backed with a two-minute TTL and can stay `true` after a server
  crash, so "still thinking" is not evidence either. The module treats fifteen seconds without
  an event as a reason to refetch, and two minutes as a reason to stop showing a typing
  indicator and say the loop is stuck.
</Callout>

## Configuration [#configuration]

| Variable                     | Notes                                           |
| ---------------------------- | ----------------------------------------------- |
| `CHATFUEL_TOKEN`             | Secret. Server-side only.                       |
| `VITE_CHATFUEL_WORKSPACE_ID` | The workspace you picked during the wizard run. |
| `CHATFUEL_API_BASE`          | Defaults to `https://panel.chatfuel.com`.       |

Every variable the app reads, and which side reads it, is on
[environment variables](/docs/deploy/environments).

## Permissions [#permissions]

| Object | Action | Required for                                                                 |
| ------ | ------ | ---------------------------------------------------------------------------- |
| `Bot`  | `View` | Creating conversations, chatting with the assistant, subscribing to updates. |

One permission, and it is the whole list. Everything conversation-scoped is ownership-checked on
top of it: conversations are per (user account, bot), and another operator's id answers
`CoworkerConversationDoesNotExist` rather than a permission error.

## Limits [#limits]

**Pagination cannot be trusted past the first page.** `hasNextPage` is wrong after it — an
off-by-one on the server — so page until you receive fewer edges than you asked for. A cursor
pointing at a deleted row answers `InternalServerError`; restart from the top. Thirty
conversations and fifty messages a page.

**The unread counter is approximate.** It increments on *every* assistant message, invisible
tool messages included, and resets all-or-nothing.

**Attachments are images and documents only.** At most fifteen files a message, fifty megabytes
each; `png`, `jpeg`, `webp`, `gif`, and PDF, Office and text documents. Video is refused
outright — the assistant cannot read it. Audio is not an attachment at all: a voice note takes
its own mutation, one file per message. Message attachments are server-side copies with a
roughly four-month lifetime.

**A conversation cannot be renamed, deleted or archived.** None of the three exists in the
public API. The server generates the title from the first user message, so it is a whole
sentence, it can be long, and it is sometimes the literal string `"null"`. What the module does
instead is keep a title and a pin in `frontendStateStorage` — server-persisted, so a rename at
the desk is a rename on the laptop. Worth knowing: &#x2A;*the assistant can read that map.** It is
the agent's own scratchpad, which is why the keys are plain words.

**One tool call in flight shows no spinner.** The wire never says which call is running, and
with a single step there is no run group to hang the state on. The typing indicator and the
composer's Stop cover it.

Four things in the API look usable and are not: `coworkerConversationCreateIceBreakers` always
returns `[]`, `coworkerConversationClickIceBreaker` panics the server,
`coworkerIncognitoConversationCreate` is for Chatfuel employees, and the skill-scoped creates
need ids that are not listable publicly.

The module's own ceilings:

|                                       |                              |
| ------------------------------------- | ---------------------------- |
| Conversations per page                | 30                           |
| Messages per page                     | 50                           |
| Files in one message                  | 15                           |
| One file                              | 50 MB                        |
| Conversation title kept in the rail   | 52 characters                |
| Params the assistant may put on a URL | 12 keys, 200 characters each |
| Navigation cooldown                   | 2 seconds                    |
| "They just asked" window              | 2 minutes                    |
| Refetch after silence                 | 15 seconds                   |
| Loop declared stuck                   | 2 minutes                    |

<Cards>
  <Card title="Copilot, as a product" href="/modules/copilot" description="What the assistant does, from the operator's side: the tools, the approval card, and what it can see." />

  <Card title="Inbox" href="/docs/modules/inbox" description="Recommended alongside this one — the conversations the assistant is usually asked about." />

  <Card title="AI Agent" href="/docs/modules/ai-agent" description="The other assistant in the product, and not this one: how the bot answers customers, rather than how the operator is helped." />

  <Card title="Your agent's notes" href="/docs/concepts/agent-skills" description="The wizard installs .claude/skills/chatfuel-coworker/ (Codex: .agents/skills/chatfuel-coworker/) — the async contract, the approval gate, screen context and frontend actions, and the don't-use list." />
</Cards>
