> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dashsquad.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Troubleshooting

> Common errors, their causes, and how to fix them.

Something not working? Here are the most common issues and how to fix them.

## Common issues

<AccordionGroup>
  <Accordion title="Missing API key">
    **Symptom:** An agent fails its first message with an authentication error.

    **Fix:** Add the provider's API key on Desktop's **AI Providers** page — it's saved to the encrypted credential store and picked up on the agent's next message. Running the HQ standalone? Set the provider's environment variable (e.g. `ANTHROPIC_API_KEY`) instead. See [AI Providers](/ai-providers#configuring-credentials).
  </Accordion>

  <Accordion title="Chat connection refused">
    **Symptom:** WebSocket connection to `ws://localhost:9200/ws/chat` is refused.

    **Fix:** Make sure the HQ is running. If using Desktop, check that the HQ process is healthy from the app. If running standalone, check the server logs for "HQ channel server listening on port 9200".
  </Accordion>

  <Accordion title="Desktop loopback chat unauthorized (4001)">
    **Symptom:** A Desktop or other loopback WebSocket connection closes immediately with
    code `4001`.

    **Fix:** Include the desktop chat token as a query parameter:
    `ws://localhost:9200/ws/chat?token=your-chat-token`. This loopback compatibility path uses the
    chat token, not the administrative management bearer. Native apps use the Mobile bearer flow
    described below instead.
  </Accordion>

  <Accordion title="Unknown agent error">
    **Error (via Chat API):**

    ```json theme={null}
    { "type": "error", "id": "...", "error": "Unknown agent: myagent" }
    ```

    **Fix:** The message references a squad member that doesn't exist. Open Desktop's **Squad** page (or call `GET /agents` on the [Management API](/api-reference#agents)) and make sure the name matches a deployed squad member.
  </Accordion>
</AccordionGroup>

## Conversation sync and resumable chat

<AccordionGroup>
  <Accordion title="Conversation is busy">
    **Symptom:** A mutation returns HTTP `409` with `conversation_busy`, or chat returns an error
    frame with the same code. The error may include an `activeTurnId`.

    **Fix:** Refresh `GET /mobile/v1/conversations/:id` and use its `activeTurnId`. Replay from
    your last applied sequence, then either send `resume` for that same turn ID or send `cancel`
    for it. Wait until a terminal frame is durable and the refreshed summary no longer has an
    active turn before retrying the mutation or starting another turn. Do not work around the
    lease by sending the prompt again with a new ID; that can duplicate model work.

    If the error says **The previous turn is still settling**, the HQ is waiting for the
    previous provider run to finish. After cancellation, cleanup can continue even when the
    conversation summary no longer has an active turn. Wait briefly before retrying. Legacy
    chat also rejects a second response while the same conversation is running on another
    connection.
  </Accordion>

  <Accordion title="No active legacy request for this connection">
    **Symptom:** Legacy chat returns **No active legacy request for this connection.** when you
    answer a question, steer a response, or send a Follow Up.

    **Fix:** Send the control on the connection that started the active request. A rejected
    message does not take ownership of a response running on another connection. Wait for that
    response to finish before starting a new request from this connection.
  </Accordion>

  <Accordion title="Chat has a sequence gap after reconnecting">
    **Symptom:** The next durable WebSocket frame has a higher `seq` than expected, or a socket
    closed while a resumable turn was running.

    **Fix:** Keep the highest sequence you applied. Request
    `GET /mobile/v1/agents/:agentId/conversations/:conversationId/events?sinceSeq=<last-seq>` and
    apply the returned entries in ascending sequence order. If the same turn is still active,
    send this frame with the original turn ID:

    ```json theme={null}
    {"type":"resume","id":"00000000-0000-4000-8000-000000000002","agentId":"agent-01","conversationId":"00000000-0000-4000-8000-000000000001","sinceSeq":12}
    ```

    Resume is idempotent. Do not resend the original `message` with a new turn ID.
  </Accordion>

  <Accordion title="Conversation revision conflict">
    **Symptom:** `PATCH` or `DELETE` returns HTTP `409` with `revision_conflict` after another
    client changed the conversation.

    **Fix:** Replace your cached conversation summary with the complete summary in
    `details.current`. Reapply the intended title or metadata change to that current value, then
    retry with its revision in a quoted `If-Match` header. Do not automatically retry a delete
    without first showing or checking the newer state.
  </Accordion>

  <Accordion title="Mobile request is unauthorized (401, 4001, or 4401)">
    **Symptom:** A management request returns HTTP `401`, direct chat closes with WebSocket code
    `4001`, or a relayed WebSocket closes with `4401`.

    **Fix:** Both native routes use the same phone-scoped Mobile bearer. Send it in the
    `Authorization` header for both `/mobile/v1` and `/ws/chat`; never put it in the URL. A hosted
    relay additionally requires `x-dash-relay-credential`, but that credential does not replace the
    Mobile bearer enforced by the HQ. Pair the phone again if either credential was revoked.
    Never paste tokens into logs or support messages.
  </Accordion>

  <Accordion title="Mobile client is rate limited (429 or 4429)">
    **Symptom:** HTTP returns `429`, a structured error reports `rate_limited`, or a relayed
    WebSocket closes with code `4429`.

    **Fix:** Stop reconnecting or retrying in a tight loop. Honor the HTTP `Retry-After` header or
    the retry delay supplied by the relay, then reconnect once. If no delay is supplied, use
    exponential backoff with jitter and reset it after a stable connection. Preserve your last
    applied sequence so the eventual reconnect replays the gap instead of resending prompts.
  </Accordion>

  <Accordion title="Relay cannot reach the HQ (502 or gateway_offline)">
    **Symptom:** The relay returns HTTP `502`, chat reports `gateway_offline`, or the phone can
    reach Dash but not the paired HQ.

    **Fix:** Bring the HQ machine online and make sure the Dash HQ is running, then let
    its outbound relay connection re-establish. Until health succeeds, show cached conversation
    history as read-only: do not queue metadata mutations, deletes, or prompts as though they were
    accepted. Once connected, refresh summaries and replay from the last sequence. Re-pair only
    if the device credential was actually revoked; an ordinary offline period does not require it.
  </Accordion>

  <Accordion title="My synced conversations are read-only">
    **Fix:** Confirm that the saved HQ is reachable. Previously synced conversations stay
    visible from cache while the HQ is offline, but cached history intentionally blocks
    creating, sending, renaming, and deleting until that HQ reconnects.
  </Accordion>

  <Accordion title="Message saved for recovery">
    **Symptom:** Dash for iOS shows **Message saved for recovery**, and that chat's composer is
    unavailable.

    **Fix:** Open **Needs Recovery** from Conversations and copy the exact message text. Preview or
    share any readable attachments before discarding the item. If Dash could not read the saved
    attachment data, the text remains available but those attachments cannot be previewed or
    shared. If a newer draft exists, Dash keeps that draft separate and places the earlier message
    here so neither payload is overwritten. The chat remains read-only until you explicitly discard
    the recovery item. Do not clear the app's data or reinstall it before preserving anything you
    need.
  </Accordion>

  <Accordion title="A conversation says Active on another device">
    **Fix:** Another paired client owns the canonical active turn. Wait for it to finish, watch the
    shared stream in Desktop, or explicitly stop it. Sending a second turn, renaming, and
    deleting are blocked until the active turn ends.
  </Accordion>

  <Accordion title="Older conversations appear under On this Mac">
    **Fix:** This is intentional. These conversations were stored locally by an older Desktop version. No automatic upload exists, and reconnecting an HQ does not move them.
    They remain readable on that computer under **On this Mac**.
  </Accordion>

  <Accordion title="Rename or delete says the conversation changed">
    **Fix:** Desktop refreshed a revision conflict because another client changed the
    canonical conversation first. Review the current title and state, then retry your rename or
    delete.
  </Accordion>

  <Accordion title="Update Dash appears in chat">
    **Fix:** The HQ returned a required contract shape this desktop version cannot decode.
    Update Desktop rather than clearing history. Your canonical conversation remains on
    the HQ.
  </Accordion>

  <Accordion title="A project session does not open">
    **Fix:** Refresh the task and confirm its HQ is reachable. Desktop fetches linked
    sessions by their canonical ID, including sessions beyond the first conversation page. A
    deleted canonical conversation stays deleted, and Desktop will not create an empty
    replacement.
  </Accordion>
</AccordionGroup>

## Secrets and credentials

<AccordionGroup>
  <Accordion title="Provider key not taking effect">
    After adding or changing a key on the **AI Providers** page, the agent still hits an auth error.

    **Fix:** The HQ reads credentials fresh on each message, so a new key takes effect on the agent's *next* message — send another one. If it still fails, confirm the key is valid with the provider and that you selected the right provider for the model the agent uses.
  </Accordion>

  <Accordion title="Credentials unreadable after copying ~/.dash elsewhere">
    Provider keys live encrypted in `~/.dash/gateway/credentials.enc`, decrypted with the key in `~/.dash/gateway/secret.key`.

    **Fix:** If you copy `~/.dash` to another machine, copy **both** files together — `credentials.enc` can't be decrypted without its `secret.key`. If `secret.key` is lost, delete `credentials.enc` and re-enter your keys on the AI Providers page.
  </Accordion>
</AccordionGroup>

## Remote access (relay)

<AccordionGroup>
  <Accordion title="Dash for iOS can't reach your Dash account service">
    **Symptom:** The HQ picker on Dash for iOS shows:

    > Couldn't reach your Dash account service. Check your connection and try again.

    **Fix:** This means loading your enrolled HQs failed — the app never confuses this with
    "no HQs enrolled," so seeing this message doesn't mean your account is empty. Check the
    phone's connection and tap **Retry**. If it persists, confirm the machine that hosts your Dash
    account service is reachable from the internet.
  </Accordion>

  <Accordion title="Dash for iOS shows no HQs linked to your account">
    **Symptom:** The HQ picker on Dash for iOS shows:

    > No HQs linked to your account yet. Open Desktop → Settings → Devices → Remote access to enroll this machine.

    **Fix:** Open **Settings → Devices → Remote access** in Desktop on the machine running
    your HQ, sign in with the same Dash account, and click **Create HQ**. Then tap
    **Refresh** on that same empty screen in the app — the new HQ appears without signing out
    or restarting. (Once the list has HQs in it, pull down on the list to refresh instead.)
  </Accordion>

  <Accordion title="Dash for iOS says an HQ needs to be re-enrolled">
    **Symptom:** Tapping an HQ in the picker shows:

    > This HQ needs to be re-enrolled from Desktop before app access works.

    **Fix:** This HQ's Remote access setup on Desktop predates this app's account
    sign-in support, so it never registered the app's chat capability. Open Desktop on
    the machine that runs this HQ — it updates the HQ's app access automatically on
    launch — then try the HQ again in Dash for iOS.
  </Accordion>

  <Accordion title="Desktop cannot create a local pairing code">
    **Symptom:** **Pair Device** shows **No usable LAN IPv4 address is available. Connect this Mac
    to the same network as the phone and try again.** Or it shows **Could not determine which LAN
    IPv4 address to use** when the computer has several active network connections.

    **Fix:** Connect this Mac to a Wi-Fi or Ethernet network the phone can reach, then reopen
    **Settings → Devices** to request a fresh code. If several networks are connected, make the
    phone-reachable network the default route or disconnect the others. Dash will not guess an
    address or put a loopback-only address in a phone pairing code. If a shared local network is
    unavailable, configure **Remote access** and use its relay pairing code instead.
  </Accordion>

  <Accordion title="Pair Device asks to switch to the local HQ">
    **Symptom:** **Pair Device** shows **Switch to the local HQ before pairing a device**
    while Desktop is connected to an HQ on another machine.

    **Fix:** Open **Settings → General → HQ** and click **Use this computer**. Wait for the
    HQ row to show **This computer**, then return to **Settings → Devices** and request a fresh
    pairing code. Desktop only generates phone pairing codes for the local HQ it can
    provision safely.
  </Accordion>

  <Accordion title="Sign-in failed">
    **Symptom:** Clicking **Sign in to Dash** on **Settings → Devices → Remote access** opens the browser but Desktop shows **Sign-in failed**.

    **Fix:** Finish signing in within the browser tab that opened — Desktop is waiting for it to hand control back. If you closed the tab, dismissed it, or it timed out, click **Sign in to Dash** again to start over. Make sure the machine running Desktop has internet access.
  </Accordion>

  <Accordion title="Select an organization">
    **Symptom:** You're signed in, but Desktop asks you to **select an organization** before you can create an HQ.

    **Fix:** HQs are owned by an organization, not by you personally. On the Dash sign-in page, create or choose an organization, then return to **Settings → Devices → Remote access** and try again. If you belong to more than one organization, make sure you're signed in to the one that should own this HQ.
  </Accordion>

  <Accordion title="Create HQ is missing or fails">
    **Symptom:** You don't see **Create HQ**, or clicking it shows **Could not enroll an HQ**.

    **Fix:** **Create HQ** only appears once you're signed in — if you see **Sign in to Dash** instead, sign in first. If it fails because the address you chose is taken, pick a different name (see "The address I want is already taken" below). Otherwise it's usually a temporary network problem: wait a moment and try again. The HQ needs to be running for it to connect out.
  </Accordion>

  <Accordion title="Remote access stopped working after a while">
    **Symptom:** Your phone reached your agents through the relay for a while, then
    couldn't connect anymore — typically after the machine was asleep or powered off
    for an extended period.

    **Fix:** Usually none needed — the HQ renews its own relay connection in the
    background and reconnects on its own, even after being offline for weeks. Just give
    it a moment after the machine wakes up. You do **not** need to re-create the HQ
    or re-pair your phone. If it still hasn't recovered after a few minutes, confirm the
    machine running Dash is online, then open **Settings → Devices → Remote access** and check that
    you see **HQ ready at** with your address. The HQ must be running for it to
    reconnect.
  </Accordion>

  <Accordion title="QR code still pairs over the local network">
    **Symptom:** The **Pair Device** section on **Settings → Devices** — used by the Dash Android
    app — says it connects over your **local network** even though you want remote access.

    **Fix:** The QR code only switches to remote access after you've created an HQ. On **Settings → Devices**, confirm the **Remote access** section shows **HQ ready at** with an address, then check the **Pair Device** section above it — the code now carries your relay address.
  </Accordion>

  <Accordion title="Phone can no longer connect after a revoke">
    **Symptom:** A phone or tablet that used to work remotely now can't reach your agents.

    **Fix:** If that device was removed under **Settings → Devices → Paired devices** (by you or
    from another machine), its credential no longer works — this is expected. On Android, pair it
    again: open **Settings → Devices**, scan a fresh QR code on the phone, and it gets a new
    credential. On iOS, open **Settings** on the phone, choose **Disconnect & Forget**, then sign
    back in and tap the HQ again in the picker — no QR code needed.
  </Accordion>

  <Accordion title="The address I want is already taken">
    **Symptom:** When creating an HQ, the address (hostname) you typed is rejected
    as unavailable.

    **Fix:** Relay addresses are globally unique and permanent — once a name is claimed
    it belongs to that HQ forever and is **never** released for reuse, even if the
    original HQ was later removed. Pick a different, more specific name (for example
    add your initials or your machine's name) and try again. The availability indicator
    in the picker tells you live whether a name is free before you commit to it.
  </Accordion>

  <Accordion title="Approving a new browser session times out or is declined">
    **Symptom:** Signing in on a new browser shows **Approve this device** with
    "Waiting for approval — scan this code with the Dash app on your phone." and a QR
    code, then instead of continuing to chat it settles on one of:

    > Approval declined. You can try again from the HQ list.

    > The code expired. Try again from the HQ list.

    **Fix:** This screen only appears on accounts that already have a Dash for iOS
    device signed in — new browser sessions need one of those devices to approve them.
    **Approval declined** means someone tapped **Deny** on the phone; **The code
    expired** means nobody approved it within the time limit. Either way, go back to
    the HQ list and sign in again to get a fresh code, then open Dash on the
    phone, go to **Settings → Approve a device**, and scan it before it expires.
  </Accordion>

  <Accordion title="Dash for iOS says a browser's code has expired">
    **Symptom:** Scanning a browser's approval code from **Settings → Approve a
    device** shows:

    > This code has expired. Ask the device to try again.

    **Fix:** Approval codes expire a few minutes after they're generated. Ask whoever
    is waiting on the browser to reload the HQ list (or sign in again) to get a
    fresh code, then scan the new one right away. A code that's still valid instead
    shows a confirm sheet reading `Allow "<device>" to access <HQ>?` — tap
    **Approve** or **Deny** there.
  </Accordion>
</AccordionGroup>

## Remote HQs

<AccordionGroup>
  <Accordion title="Desktop is still using the local HQ">
    **Symptom:** You deployed or entered a remote HQ, but Desktop still
    shows agents from the local machine.

    **Fix:** Open **Settings → General → HQ** and check the active HQ
    status. If it says **This computer**, click **Change HQ**, choose
    **Connect existing HQ**, test the connection, then click **Use this HQ**.
    If you want to return to local mode, click **Use this computer**.
  </Accordion>

  <Accordion title="Remote HQ is unhealthy">
    **Symptom:** **Settings → General → HQ** shows an existing HQ
    with an unhealthy status.

    **Fix:** Confirm the management URL is reachable, the management token is
    correct, and the relay credential is still valid. Click **Change HQ** and
    test the connection again before using it. For a VPS deployment, SSH to the
    server and run:

    ```bash theme={null}
    systemctl --user status dash-gateway.service
    ```
  </Accordion>

  <Accordion title="VPS deployment fails">
    **Symptom:** **Deploy and connect** fails before the HQ is saved.

    **Fix:** Open **Settings → General → HQ**, click **Change HQ**, then
    choose **Self-host on a VPS**. Make sure the SSH host, user, port, and key are
    correct. The VPS must have `git`, `node`, `npm`, and `systemctl --user`
    available. If the relay URL or HQ id is wrong, fix those fields and
    deploy again.
  </Accordion>

  <Accordion title="Chat opens but messages do not stream">
    **Symptom:** Desktop can list agents, but chat messages hang or fail.

    **Fix:** Open **Settings → General → HQ**, click **Change HQ**, choose
    **Connect existing HQ**, and test the connection again. The chat URL should
    use `wss://` for HTTPS relays and point at the same HQ host as the
    management URL.
  </Accordion>
</AccordionGroup>

## Tool errors

<AccordionGroup>
  <Accordion title="Workspace not found">
    If a tool reports it can't find files, check that the `workspace` path in your agent config exists. Dash creates the workspace directory automatically at startup, but if the config path is wrong, the tool may be looking in the wrong place.
  </Accordion>

  <Accordion title="Path escape blocked">
    **Error:**

    ```
    Error: path "../../etc/passwd" escapes the workspace directory
    ```

    The `read` tool blocks paths that resolve outside the workspace. This is intentional — use absolute paths or paths relative to the workspace root.
  </Accordion>

  <Accordion title="Command timeout">
    **Error:**

    ```
    Command failed
    ```

    The `bash` tool has a 30-second timeout. Long-running commands are killed after this period. Break long operations into smaller steps or increase the timeout in the source code if needed.
  </Accordion>

  <Accordion title="Output truncation">
    The `bash` tool limits output to 100 KB. If a command produces more output, pipe it through `head`, `tail`, or redirect to a file and use `read` to retrieve specific sections.
  </Accordion>
</AccordionGroup>

## Plugins

<AccordionGroup>
  <Accordion title="Plugin not loading">
    A plugin you installed isn't showing its skills or commands.

    **Fix:** Open Desktop's **Plugins** screen and check the plugin's status badge. **Disabled** means it's turned off — click **Enable**. **Error** means it failed to load — the card shows the failure message (usually a bad manifest or a missing `SKILL.md`). If hand-editing, make sure the plugin has an entry in `~/.dash/gateway/plugins/config.json` with `"enabled": true`. See [Plugins](/plugins#managing-plugins-in-desktop).
  </Accordion>

  <Accordion title="Code components inert until trusted">
    A plugin's MCP servers, hooks, `bin/` executables, or providers do nothing, even though the plugin is enabled.

    **This is intentional.** A freshly installed plugin is enabled but **not trusted** — its skills and commands work, but its code and credential components stay inert until you trust it. Click **Trust** on the plugin card, review the confirmation listing what will run, and confirm. See the [Security model](/plugins#security-model).
  </Accordion>

  <Accordion title="Slash command /plugin:command not found">
    Typing `/<plugin>:<command>` doesn't resolve.

    **Fix:** Commands are namespaced as `/<plugin>:<command>` — a plugin named `research` with `commands/triage.md` is `/research:triage`, not `/triage`. Confirm the plugin is **enabled** (commands need only `enabled`, not trust) and **Loaded** on the Plugins screen, and that the file lives in the plugin's `commands/` directory. See [Invoking each component](/plugins#invoking-each-component).
  </Accordion>

  <Accordion title="OpenRouter model list looks outdated">
    Open the model picker again after updating your HQ. The HQ refreshes
    its provider list on the first request after six hours, and sooner when its
    bundled catalog changes. Desktop's refresh action can fetch it immediately.

    Dash curates models for chat and tool use. A newly released model may need a
    catalog update before it appears. Free/batch variants and unreviewed previews
    are not automatically included. Make sure you are viewing **OpenRouter**;
    a model under a direct provider is a separate entry.

    If OpenRouter is temporarily unavailable, Dash keeps the last successful list
    and retries when the picker is opened after five minutes.
  </Accordion>

  <Accordion title="Plugin provider model not in the dropdown">
    A model from a plugin's LLM provider doesn't appear in the agent's model picker.

    **Fix:** Plugin providers need **two** things: the plugin must be **trusted** (an untrusted plugin's provider catalog is withheld entirely), and you must add an **API key** for it on the **AI Providers** screen. Trust the plugin, add the key, then the provider's `<id>/<model-id>` models become selectable. See [Provider credentials](/plugins#provider-credentials).
  </Accordion>

  <Accordion title="Install failed">
    `POST /plugins/install` (or the **Install** button) returned an error.

    **Fix:** The response code tells you why. **404** — the source wasn't found (a missing repo, dead URL, or unknown name); check the source string. **409** — a plugin with that name is already installed; remove it first or install under a different `name`. **422** — the plugin is invalid, corrupt, or the security scan flagged it as **dangerous** and refused it; inspect the source before retrying. See [What the errors mean](/plugins#what-the-errors-mean).
  </Accordion>

  <Accordion title="Removed plugin still listed">
    A plugin you removed still appears, or a hand-dropped plugin doesn't show up.

    **Fix:** The HQ hot-reloads on changes made through Desktop or the management API, but it doesn't watch the folder for files you add or delete by hand. After editing `~/.dash/gateway/plugins/` directly, trigger a rescan with **POST /plugins/reload** (or restart the HQ). Note that removing a plugin only deletes its directory when it was installed by the management API — a hand-dropped or `path`-linked plugin is left on disk by design.
  </Accordion>
</AccordionGroup>

## Sub-agents

<AccordionGroup>
  <Accordion title="A sub-agent didn't start (cap reached)">
    An agent tried to launch a sub-agent and got an error instead. In chat this appears as a **red error tool block** on the `agent` (or `spawn_worker`) call.

    **This is a cap doing its job.** The message tells you which one:

    * `too many workers running at once (max N) — wait for workers to finish` — the agent's **concurrent** limit.
    * `swarm run reached its worker limit (N workers per run)` — the **total-per-turn** limit.
    * `the HQ is at its global worker limit (N) — wait for workers to finish` — the HQ-wide ceiling across **all** agents.

    Raise the relevant cap in the agent's `subagents` block if the work genuinely needs more — see [Configuration → Sub-agents](/configuration#sub-agents). The change takes effect on the agent's next message. The HQ-wide ceiling (default `16`) is raised by setting `SUBAGENTS_MAX_CONCURRENT_GLOBAL` where the HQ runs and restarting it — see [Configuration → HQ defaults](/configuration#hq-defaults).
  </Accordion>

  <Accordion title="Agent would be spawned with zero tools">
    **Error:**

    ```
    Agent would be spawned with zero tools: mcp__github, write
    ```

    A sub-agent type asked for tools that its parent does not have, so the intersection came out empty — and Dash refuses to run a tool-less agent rather than launching one that can do nothing. The message names the entries that resolved to nothing.

    **Fix:** either give the parent agent those tools (Desktop → the agent's **Configuration** tab → **Tools**, and its assigned MCP servers for `mcp__…` entries), or edit the type's `tools:` frontmatter to ask for something the parent has. A sub-agent can never hold a tool or MCP server its parent lacks — see [Configuration → Sub-agent types](/configuration#sub-agent-types).
  </Accordion>

  <Accordion title="depth limit reached">
    **Error:**

    ```
    depth limit reached (maxDepth 3) — an agent at depth 3 cannot spawn a depth-4 agent
    ```

    Sub-agents may nest, but only so far. A direct child is depth 1, and the ceiling is `subagents.maxDepth` (default `3`).

    **Fix:** raise `maxDepth` for that agent, or have the work done one level up. Note that an agent already at the ceiling is not given the `agent` tool at all, so in most cases it simply won't try. Separately, chat shows **one level** of nesting: expanding a sub-agent shows its transcript, and a row inside that transcript reads *"Nested agents this deep are not opened here."* — that is the display limit, not the run limit.
  </Accordion>

  <Accordion title="alias &#x22;opus&#x22; is not configured (subagents.modelAliases)">
    **Note on the tool result:**

    ```
    Note: alias "opus" is not configured (subagents.modelAliases); using the parent model
    ```

    The `agent` tool's `model` parameter accepts short alias names as well as full model ids. An alias is looked up in the agent's `subagents.modelAliases`, and **no aliases are configured by default** — which model `opus` should mean depends on which provider the agent uses, so nothing is assumed. An alias that isn't in the map resolves to nothing, the sub-agent runs on the parent's model, and the note above is appended to the tool result. Nothing failed.

    **Fix:** add the alias to the agent's `subagents.modelAliases` (for example `{"opus": "anthropic/claude-opus-4-6"}`), or pass a full `provider/model` id (or `inherit`) instead. Either way, if the id isn't the parent's model or one of its fallbacks, add it to the agent's `subagents.allowedModels` first — see [Configuration → Sub-agents](/configuration#sub-agents).
  </Accordion>

  <Accordion title="My sub-agent stopped waiting for an answer">
    A sub-agent asked a question with `ask_orchestrator`, showed as **waiting for input**, and then carried on — or finished — without anyone answering. Its own transcript shows the call coming back as `ask_orchestrator aborted`.

    **A question lives only as long as the turn that launched the sub-agent.** When that turn ends, any question still open is dropped so the sub-agent is not left waiting on a conversation that has moved on. A foreground sub-agent holds its parent's turn open until it finishes, so there is time to answer from the reply box on its row. A **background** sub-agent usually outlives the turn that launched it, so a question it asks after that turn has ended has nobody to answer it.

    **Fix:** answer while the turn is still running, or launch the sub-agent in the foreground when you expect it to need a decision. A brief that tells the sub-agent what to assume when it cannot ask is the sturdier fix for background work.
  </Accordion>

  <Accordion title="isolation: worktree requires a git workspace">
    **Error:**

    ```
    isolation: worktree requires a git workspace
    ```

    `isolation: worktree` gives a sub-agent its own checkout of the workspace, cut from whatever commit the workspace is on right now. That needs the workspace to be inside a git repository.

    **Fix:** run `git init` in the agent's working directory (or point the agent at a directory that is already a repo), or drop `isolation` and let the sub-agent share the parent's workspace.
  </Accordion>

  <Accordion title="My sub-agent's worktree wasn't removed">
    A directory is still sitting under `~/.dash/gateway/worktrees/<agent-name>/<subagent-id>/` after the sub-agent finished.

    **This is by design when the worktree holds work.** Cleanup removes a worktree only when there is nothing to lose: uncommitted changes, untracked files, and commits that no branch or tag would keep alive all keep it. (Regenerable litter — `node_modules/`, `dist/`, `build/`, `.next/`, `coverage/`, `*.log`, `.DS_Store` — does not count.) The path is not handed back to you in the chat: to find it, either fill in the template above with the sub-agent's id, or look in the HQ log, where every kept worktree is recorded with a line containing `keeping` and the path, and why it was kept.

    **Fix:** look at what's inside and keep or merge what you want. Once the checkout is clean, `git worktree remove <path>` from the parent repository takes it down; git refuses while there are changes in it, so commit or discard them first (or pass `--force` if you are sure).
  </Accordion>

  <Accordion title="The HQ restarted while sub-agents were running">
    The HQ process died (crash, force-quit, machine reboot) while sub-agents were still working.

    **Recovery is automatic.** On its next start the HQ marks every sub-agent that was still running as **interrupted**, keeps its transcript, and tells its parent agent — as a notification turn delivered as soon as the HQ is serving again (or with the parent's next turn, if it's busy). The rows stop spinning and show `interrupted`.

    Unlike earlier releases, an interrupted sub-agent is **not lost**: press **Resume** on its row — from the web tasks panel, the iOS tasks sheet, or `POST /subagents/:id/resume`; Desktop greys the button once a sub-agent is terminal — or have the parent `send_message` it, and it picks up with its context intact. Orphaned worktrees are swept at the same time, except ones holding work.
  </Accordion>

  <Accordion title="Resume is refused or greyed out">
    A **Resume** — from a row, from the tasks list, or from the parent's own `send_message` — comes back with an error instead. There are three common reasons, and a resume also counts against the launch caps, so the two cap messages described under *"A sub-agent didn't start (cap reached)"* above can arrive here too:

    * `Agent "..." is a one-shot Explore agent and cannot be resumed.` — `Explore` and `Plan` are read-only, one-shot types, so they cannot be given a new task. **Fix:** launch a new one with the follow-up task. (If the agent is still running and asking a question, that is an *answer*, not a resume, and it goes through: the parent's `send_message`, the reply box on its row and the legacy `send_to_worker` all reach a paused one-shot.)
    * `steer cap reached for "..." (max 10 per agent).` — you've hit the per-child message cap, which counts across resumes for as long as the HQ holds that child in memory (a restart resets it to zero). **Fix:** raise `swarm.maxSteersPerWorker` for the agent, or start a fresh sub-agent.
    * `Agent "..." cannot be resumed: its grant cannot be rebuilt.` — the parent's grant could not be re-established. Either **sub-agents have been turned off** on the parent agent (or the agent itself was **deleted** — its conversations survive the delete, but nothing under them can run again), or the parent conversation (or one above it) was **deleted** — deleting a conversation stops everything under it from running again. A sub-agent created by an **older HQ build**, before the grant was stored beside the row, has the same symptom. **Fix:** re-enable sub-agents on the parent agent — see [Configuration → Per-agent settings](/configuration#per-agent-settings). A deleted parent, a deleted agent and a grant that was never stored cannot be recovered, so launch a new sub-agent instead.

    A resume also **silently** re-intersects the child's tools and MCP servers with what its parent holds now, so a resumed sub-agent can come back with fewer tools than it had, without any message saying so. Its **model** is not re-checked: a child keeps the model it was launched with even if that model has since been removed from the parent's allowed set.

    Desktop's panel also greys **Resume** out for a sub-agent that has already finished; use the web tasks panel or `POST /subagents/:id/resume` to resume one from there.
  </Accordion>

  <Accordion title="Where are the sub-agent transcripts?">
    A sub-agent is a conversation of its own, so its transcript is where every other conversation is:

    ```
    ~/.dash/gateway/sessions/<agent-name>/<subagent-id>/
    ```

    Read it in the app instead: expand the sub-agent's row in chat, or open it from the tasks panel. Transcripts are **never auto-deleted**. Older releases wrote workers under `~/.dash/gateway/sessions/<agent-name>/.swarm/<runId>/`; those directories are no longer written to and are left alone — remove them by hand if you want the space.
  </Accordion>

  <Accordion title="No sub-agent progress on Telegram / WhatsApp">
    You chat with an agent over a messaging app and see only the final answer — no per-sub-agent updates.

    **This is by design.** Sub-agent rows, the tasks panel and sheet, and Desktop's **Sub-agents** panel are chat-app features (iOS, the web client, and Desktop). Messaging channels deliver only the agent's **final response**, not the intermediate sub-agent events. To watch and steer sub-agents in real time, chat with the agent in one of those clients.
  </Accordion>

  <Accordion title="An old conversation's cancelled worker lost its reason">
    Conversations recorded before this release replay fine — each worker still draws exactly one card, in the same place, with the same status. One narrow case loses detail: a worker that was **cancelled after its viewer had already gone away** recorded its reason only on an event type that no longer exists, so its card now settles on `cancelled` without the explanatory note. Nothing else about the replay changed, and nothing needs fixing.
  </Accordion>
</AccordionGroup>

## Session issues

<AccordionGroup>
  <Accordion title="Provider session JSONL is corrupt">
    Provider session JSONL stores the model's working context separately from the canonical
    conversation that Desktop and paired phones display. If a provider session file has an
    invalid line after a crash, the agent may fail to restore model context even though canonical
    history remains visible. To recover:

    1. Find the conversation's session directory: `~/.dash/gateway/sessions/{agent-name}/{conversationId}/` and open the `.jsonl` session file inside it
    2. Open it in a text editor
    3. Remove or fix the malformed line(s) — each line should be a valid JSON object
    4. Restart Dash

    Since provider session JSONL is append-only, you can remove malformed lines from the end
    without damaging earlier model context. This does not edit the canonical rendered messages.
  </Accordion>

  <Accordion title="Starting with fresh provider context">
    To reset only the agent's provider session state, delete its session directory:

    ```bash theme={null}
    rm -rf ~/.dash/gateway/sessions/{agent-name}/{conversationId}
    ```

    To reset all provider session state:

    ```bash theme={null}
    rm -rf ~/.dash/gateway/sessions
    ```

    These commands do not delete canonical conversations shown by Desktop or paired phone
    apps. To remove shared history, reconnect the HQ and delete the conversation in Dash.
  </Accordion>

  <Accordion title="My data moved after updating">
    Dash now keeps everything under `~/.dash` (HQ data, desktop app data, logs, and agent workspaces). The first time a newer version starts, it automatically moves data from the old locations into `~/.dash` — your credentials, agents, and conversations come along.

    If something looks missing, check the old locations:

    * macOS: `~/Library/Application Support/dash` and `~/Library/Application Support/dash-gateway`
    * Linux: `~/.local/share/dash` and `~/.local/share/dash-gateway`
    * Agent workspaces: the old `~/dash-workspaces`

    The migration only runs when `~/.dash` doesn't already have that data, and it never deletes the originals — so it's always safe to copy anything left behind by hand. To pin Dash to a specific location instead, set `DASH_HOME`.
  </Accordion>
</AccordionGroup>

## Docker issues

<AccordionGroup>
  <Accordion title="Data lost on restart">
    Dash stores everything under `DASH_HOME` (set to `/data` in the container). Make sure that path is backed by a persistent volume in `docker-compose.yml`:

    ```yaml theme={null}
    environment:
      - DASH_HOME=/data
    volumes:
      - dash-data:/data
    ```

    Without this volume, credentials, agents, and sessions live only inside the container and are lost when it's recreated.
  </Accordion>
</AccordionGroup>

## Debug logging

Desktop and the HQ write logs to `~/.dash/logs/` (`mc.log` and `gateway.log`) — check there first when something goes wrong.

For a standalone HQ, add the `--verbose` flag to get debug-level output:

```bash theme={null}
npm run gateway -- --verbose
```
