DK Studio/Documentation
ProductChangelog

Agent offline

An agent is offline when the studio has no live socket for it: it does not appear in the agent list, or a command answers Agent is offline. The checklist below works through that situation in four short passes over the network, the service, the authorisation and the recovery β€” because every one of these causes produces the same symptom. Work the passes in order and stop when the agent comes back.

Symptoms

What you seeWhat it points at
The agent is missing from the listThe studio holds no live socket for it. Presence is derived from the live socket and is never stored.
POST /api/agents/command β†’ 400 {"error":"Agent is offline"}The command is refused before it is queued, for the same reason.
GET /api/agents β†’ {"agents":[]}No agent holds a live socket.
POST /api/agents/command β†’ 404 {"error":"Agent not found"}The agentId you sent is not one the studio knows.
POST /api/agent/register β†’ 403The licence is unknown, revoked, expired, or the secret does not match the token.
POST /api/agent/auth β†’ 401 {"error":"Invalid API key"}The key in data/agent.json is not the one the studio issued.
The agent log reads Authentication failedThe socket handshake was rejected. The studio answers the auth request with ok: false and the code AUTH_FAILED, and closes the socket.

Two endpoints answer most of your questions: GET /api/agents lists the agents that hold a live socket, and GET /api/admin/agents lists every licence and every registered agent with its connectionStatus.

curl -sS http://localhost:3000/api/agents
curl -sS http://localhost:3000/api/admin/agents

Check the network

The agent dials out to the studio; the studio never dials an agent. Only http and https studio URLs work, and the address in studioUrl must be reachable from the bench, not from the laptop you are sitting at.

The address lives in data/agent.json on the agent, alongside agentId, apiKey and agentSecret. The AGENT_NAME and STUDIO_URL entries in the agent's .env are not read, so editing them changes nothing β€” edit the file.

  1. From the bench, reach the studio's REST API. A JSON body with a status field means the studio is up and the path works.
  2. From the bench, reach the socket endpoint on the same port. A Socket.IO handshake starts with 0{"sid":…}; a 404, an HTML page or a refused connection means the socket endpoint is not served on that URL.
  3. Ask the agent what it thinks. {"connected":false} means it has never reached the studio.
curl -sS http://studio.lab.internal:3000/api/system/status
curl -sS "http://studio.lab.internal:3000/api/socket.io/?EIO=4&transport=polling"
curl -sS http://localhost:3001/api/connect

The two ports involved are the studio's PORT (3000 by default, carrying the UI, the REST API and the socket endpoint) and the agent's own AGENT_PORT (3001), which stays on the bench.

Check the service

The studio serves the web UI, the REST API and the realtime socket from one server on one port, with the socket endpoint mounted at /api/socket.io. Two things follow from that.

  1. Confirm the studio answers on its socket path. If the REST API answers but the socket path does not, the process you started is not serving the socket endpoint. Check how the studio was started, and check that PORT in the studio's environment matches the port in the agent's studioUrl. The studio prints its socket path at startup: > WebSocket path: /api/socket.io.
curl -sS "http://studio.lab.internal:3000/api/socket.io/?EIO=4&transport=polling"
  1. Confirm the studio can reach its own database. The socket handler reads the agent records from the database the studio is configured with, so a database the studio cannot open makes every handshake fail. The configuration lives in .db-config.json in the studio's working directory β€” dbType, host, port, database, username, password β€” and the studio reads that file relative to its own working directory. Start the studio from the directory that holds it.
cat .db-config.json

Check the authorisation

Three credentials are involved, and different endpoints check them. The licence code is checked by POST /api/agent/register. The agent secret is checked by the socket handshake. The API key is checked by POST /api/agent/auth, an endpoint the agent itself never calls.

  1. Register again with the studio URL, the agent name and a licence code, either from the agent's page at http://localhost:3001 or directly:
curl -sS -X POST http://localhost:3000/api/agent/register \
  -H 'Content-Type: application/json' \
  -d '{"code":"<licence-code>","name":"bench-01","studioUrl":"http://studio.lab.internal:3000"}'

The 403 body names the reason: invalid_code (no licence carries that code), used (the licence was already spent on another machine), revoked, or expired.

  1. Check the secret the handshake compares. The socket handshake compares agent_secret only. A valid API key with a wrong secret still produces an AUTH_FAILED answer.

  2. Give every bench its own licence. A licence authorises one registration. Presenting the same code again is refused, so a licence never creates a second agent. To bring an agent back with the credentials it already holds, register with its agentId and agentSecret β€” that updates the agent in place, keeps its agentId and apiKey, and increases registrationCount.

  3. Know what a revocation does. A revoked licence stops further registrations. Restarting the agent is what takes a connected agent down, so that it has to authenticate again.

Recover

  1. Re-register and restart

    Register again with the studio URL, the agent name and a licence code. A successful registration answers with agentId, apiKey and agentSecret, which the agent stores in data/agent.json. Restart the process so it reads that file and dials the studio again: pnpm --filter @dkstudio/agent dev.

  2. Verify presence and the command path

    The agent should appear in both places β€” GET /api/agents contains its id with status online, and GET /api/admin/agents shows connectionStatus: "online". Then send a command with POST /api/agents/command and read the result at GET /api/agents/commands/{id}.

  3. Confirm the polling fallback

    The agent polls GET /api/agent/commands?agentId= every 5000 ms and posts results to POST /api/agent/commands/result. Both paths write the same agent_commands rows, so a command still gets through when the socket is down β€” it arrives up to five seconds later. The agent log line Starting command polling every 5000ms tells you the loop is running.

Presence is not lastSeen

Whether an agent is online is derived from its live socket. The lastSeen column next to it is written by POST /api/agent/heartbeat and is not a liveness signal.

See also