Skip to content

Remote Workers

Remote workers let you offload agent execution to machines other than the one running the Trimus server. The server remains the single source of truth for scheduling, the database, and the UI. Remote machines connect outbound to the server over HTTPS — no VPN, no database access, no inbound firewall changes on the server side.

This is the wire-protocol reference. For writing a worker from scratch in any language, see docs/user/integrators/building-a-worker.md. For an executable, step-by-step validation with curl + jq, see docs/user/operators/remote-worker-smoke-test.md.

How it works

┌─────────────────────────────────┐
│         Trimus Server           │
│  scheduler · DB · UI · API      │
│                                 │
│  POST /api/workers/tasks/claim  │◄──── polls ──── Remote Worker (Mac Mini)
│  POST /api/workers/tasks/{id}/  │◄──── result ─── Remote Worker (MacBook Pro)
│    complete | fail              │◄──── polls ──── Remote Worker (Cloud VM)
└─────────────────────────────────┘
  1. An agent is configured with adapter_type: remote on the server.
  2. When the agent's heartbeat fires, the engine's remote adapter queues a remote task in the database with the agent's required tags.
  3. A remote worker polls POST /api/workers/tasks/claim, receives a task whose required tags it satisfies, and executes it locally using whichever adapter it implements (llm, claude_local, docker, or your own).
  4. The worker posts the result to POST /api/workers/tasks/{id}/complete (or /fail).
  5. The server records the result, applies the agent's action verbs, and posts the prose as a comment on the issue — exactly as if the agent had run in-process.

Workers authenticate with a dedicated worker API key. They never touch PostgreSQL.

The endpoints

When Method + Path Auth Returns
Once, at startup POST /api/workers/register none (public) worker_id, api_key, operating_prompt, prompt_version
Every ~30 s POST /api/workers/heartbeat worker key 204
Whenever idle POST /api/workers/tasks/claim worker key 204 (no work) or 200 + task
During execution (poll) GET /api/workers/tasks/{taskId}/control worker key 200 + {"cancel": bool}
During execution (optional) POST /api/workers/tasks/{taskId}/events worker key 204
After success POST /api/workers/tasks/{taskId}/complete worker key 204
On error POST /api/workers/tasks/{taskId}/fail worker key 204

The two in-flight endpoints are optional for a minimal worker, but without them the worker never observes server-side cancels and produces no live transcript. …/control (#111) is the cross-process cancel channel: poll it during execution (the shipped binary polls every 3 s); {"cancel": true} means the server requested cancellation — kill the subprocess and do not call /complete or /fail, since the task is already cancel-requested/cancelled server-side (the shipped worker skips the result report entirely). …/events (#108 SP-6) streams engine events — body {"events":[{"kind": ..., "payload": ...}]} — into the same run transcript and run.log SSE channel as an in-process run. Both return 404 (not 403) when the task is unknown or was claimed by a different worker.

POST /api/workers/register is public (in the rate-limited, no-auth group): registration is what issues the worker key, so it can't require one. Registrations are capped instance-wide by TRIMUS_MAX_REMOTE_WORKERS (default 10000; 0 disables the cap) — the endpoint refuses fresh registrations with 503 once the cap is reached, though idempotent re-registrations still succeed. As of #148, GET /api/workers/operating-prompt is behind WorkerAuth — the full contract shouldn't be disclosed anonymously, and it's already delivered in the register response, so only an already-approved worker re-fetching after an upgrade needs it. All the authenticated endpoints require Authorization: Bearer <api_key> and are gated by WorkerAuth (which resolves the key against remote_workers and, under #210, 403s a worker that is disabled, pending, or rejected). GET /api/workers (the admin list) uses session/bearer auth, not WorkerAuth.

When TRIMUS_REQUIRE_WORKER_APPROVAL=true (#210), a fresh register returns {"worker_id", "api_key", "status":"pending_approval"} with no operating_prompt — the key is inert (heartbeat/claim 403) until an instance-admin approves the worker on /admin/workers, after which the next register resumes as approved and delivers the prompt.

The operating contract

Workers don't have to learn the protocol from this document alone. The server ships a canonical operating prompt that documents the lifecycle, the full task envelope, the action vocabulary, and the operating rules:

  • It is embedded verbatim in the registration response (operating_prompt), so a fresh worker has the entire contract after one round-trip.
  • It is also served at GET /api/workers/operating-prompt (behind WorkerAuth as of #148) for long-running workers that want to re-fetch after a server upgrade, using their existing worker key.
  • It is rendered on the /admin/workers UI page with a copy button so operators can paste it into a worker's system-prompt template.

The prompt carries its own semver (prompt_version), independent of the Trimus binary version, so workers can detect contract drift even when nothing else changed. Current version: 1.4.0 (source: internal/engine/remoteprompt/version.go, re-exported by internal/engine/remote_prompt.go). 1.4.0 (#336) made worker tags server-authoritative and operator-editable on /admin/workers; a claim-body tags set may only narrow to a subset of the worker's stored tags, never broaden past them. It is a restriction of previously-permissive behaviour (the shipped binary sends no claim-body tags), so older workers keep interoperating. 1.3.0 (#108 SP-5) added the optional full-context payload fields — capabilities, comments, activities, children, structured wake, issue attachments — plus a full_context flag; it is additive and opt-in, so older workers keep rendering the thin prompt.

Server setup

1. Run migrations

Migrations run automatically on server startup. Once you deploy a server binary that includes the remote-worker migration, the remote_workers, remote_worker_keys, and remote_tasks tables exist.

2. Register a worker

Instance admins can skip the curl entirely: /admin/workers → Register worker (#335) pre-provisions the worker server-side and renders a one-time credentials page with the freshly minted api_key and a ready-to-paste env file — TRIMUS_SERVER_URL (from TRIMUS_PUBLIC_URL) and TRIMUS_WORKER_KEY already filled in, the name/tags/concurrency you entered prefilled, and the remaining optional knobs commented out with their defaults.

  • The key is shown once, exactly like the API path — Trimus stores only its hash.
  • Workers minted here are born approved, even when TRIMUS_REQUIRE_WORKER_APPROVAL=true: the admin's click is the admission decision, so the #210 pending queue never applies.
  • The TRIMUS_MAX_REMOTE_WORKERS cap applies exactly as on the public endpoint.
  • The shipped trimus-worker binary presents the minted key on its boot-time register call and resumes this worker (idempotent — see below): same worker_id, no duplicate row, no new key.
  • Until the worker first connects, the row shows never under Last Seen. A minted-but-never-deployed key is a dangling credential — delete rows you don't end up using.

From the API

Call the registration endpoint once per worker machine, from anywhere that can reach the server.

curl -s -X POST https://your-trimus-server/api/workers/register \
  -H "Content-Type: application/json" \
  -d '{
    "name": "mac-mini-m4",
    "tags": ["darwin", "claude_local", "high-memory"],
    "version": "3.13.0",
    "commit": "6a2ca38",
    "build_date": "2026-06-05T18:22:15Z",
    "prompt_version": "1.4.0"
  }'

The version, commit, build_date, and prompt_version fields are optional and self-reported by the worker (the shipped trimus-worker fills them from its build metadata; prompt_version is the contract revision it was compiled against). They are recorded on the worker row and shown in the /admin/workers table so an operator can spot workers that are behind the server. Omitting them leaves the columns blank (); they update on every (re-)registration, so a restarted worker refreshes its reported version.

Response:

{
  "worker_id": "550e8400-e29b-41d4-a716-446655440000",
  "api_key": "sk_<64 hex chars>",
  "operating_prompt": "# Trimus Remote Worker Operating Contract\n\nYou are a remote worker...",
  "prompt_version": "1.4.0"
}

Save the api_key immediately — it is shown only once. Store it on the worker machine with tight permissions (e.g. ~/.config/trimus-worker/env, mode 0600). Worker keys have the same format as every Trimus API key: sk_ + 64 hex characters.

operating_prompt is the canonical worker contract; a fresh worker should save it (or use it directly as its model's system prompt). The same string is served at GET /api/workers/operating-prompt.

tags are free-form strings describing the machine's capabilities. Suggested conventions:

Tag Meaning
darwin macOS — can run claude_local
linux Linux — can run Docker containers
claude_local claude CLI installed
docker Docker daemon available
gpu NVIDIA / Apple GPU available
high-memory 64 GB+ RAM

3. Configure an agent to use remote execution

In the UI: open the agent's page (or the New agent form), set Adapter to Remote (worker), and fill in the revealed fields — required worker tags, the local adapter the worker runs internally, and the timeout. You can also set the company-wide default adapter to remote under Company settings → Agent defaults.

Via the API (equivalent): set the agent's adapter_type to remote and provide adapter_config, on create (POST /api/companies/{companyId}/agents) or update (PATCH /api/agents/{agentId}):

{
  "adapter_type": "remote",
  "adapter_config": {
    "local_adapter_type": "claude_local",
    "tags": ["darwin", "claude_local"],
    "timeout_seconds": 600
  }
}
Field Default Description
local_adapter_type "llm" The adapter the worker runs internally. The shipped binary supports llm, claude_local, docker. A custom worker can run anything.
tags [] The worker must have all of these tags to claim the task. Empty agent tags inherit the project's default_worker_tags, then the company's (#261) — see Worker affinity defaults.
timeout_seconds 600 How long the engine waits for a worker to complete the task.

If tags is empty at every tier (agent, project, and company), the task is claimed by whichever eligible worker polls first — useful for load-balancing across identical machines.

The task payload

When a worker claims a task, the payload object carries full parity with what an in-process adapter would receive at the same heartbeat:

Field Type When populated Meaning
run_id, agent_id, agent_name, agent_role string always Run + agent identity.
manager_name, manager_role string | null when the agent's reports_to is set The /assign escalation target.
company_id, company_name string always Tenant identity.
adapter_type, adapter_config string + JSON always What adapter the agent was configured for. Informational — the worker may run a different one.
system_prompt string always The agent's persona. Use directly as your model's system message.
wake_reason string always What triggered the heartbeat (manual, wakeup: <text>, schedule, …).
issue.id, issue.identifier, issue.title, issue.status, issue.priority, issue.body_markdown string when an issue is assigned The issue under work.
issue.project_name string when the issue has a project Project name.
issue.labels string[] when labels are attached Alphabetically sorted.
issue.timeline object[] when there is history or activity Ordered comments + activity-log entries. Each entry has type ("comment" or "activity"), author_name, body, kind (activities), and payload (raw activity JSON).
roster object[] routing roles only (e.g. triage) Teammates available for /assign: id, name, role, title, description.
session_id, session_cwd string on resumable CLI adapters Prior session to resume (claude_local / codex_local).
allow_dangerous_permissions bool | null when the server resolved the policy (#74) Tri-state gate for the Claude CLI --dangerously-skip-permissions flag: true = the worker may pass it, false = the worker must strip it even when adapter_config asks for it, absent = legacy behavior (worker re-derives from adapter_config + its own env).
skills object[] when the merged skill tree is non-empty (#70) The instance→company→agent skill files, materialised server-side; the worker rehydrates them to disk and passes --add-dir.
project_repo_url string when the project has a repo_url (#71) Git remote the worker clones/pulls on demand and uses as the CWD.
project_id, project_file_backend, project_module_endpoint, project_module_token string when the project uses file_backend=external_module (#110) Project-module wiring. The token is a short-lived, project-scoped credential — the worker never holds the master key.
staple_api_key, staple_base_url string on subprocess adapters (#108 SP-2) Per-run ephemeral Trimus API key (revoked when the run ends) + base URL, injected into the agent subprocess env as TRIMUS_API_KEY / TRIMUS_BASE_URL so callback skills work remotely. Field names intentionally keep the historical staple_ spelling — they are a wire-format contract with already-deployed remote workers; renaming them would break payload parsing on workers running an older binary.
knowledge object[] when RAG retrieval returned chunks (#97) Company-knowledge chunks; rendered as the same ## Company Knowledge prompt section as in-process runs.
compiled_index string when Compiled Knowledge is enabled (#220) Pre-built Compiled Knowledge index for the ## Compiled Knowledge prompt section.
full_context bool when the agent opted into full context (#108 SP-5) When true, the rich fields below are populated and the worker renders the full ModeFull prompt; when absent, the worker stays on the thin prompt.
capabilities, comments, activities, children, wake object[] / object only when full_context is true The full-context field group (prompt contract v1.3.0), copied 1:1 into a ModeFull render request.
egress_allowlist, egress_proxy, egress_dns_addr string[] / string when the company has an egress policy (#79) Outbound-network routing for the agent subprocess. Best-effort routing for cooperative agents — not a hard sandbox.

All optional fields use omitempty — absent when there's no data. Workers built against older payload shapes keep working; they just don't see the newer context. The full envelope (with example JSON) is §8 of the operating prompt.

The result payload

The body you POST to complete is a RemoteTaskResult:

{
  "summary": "<model response text, INCLUDING action lines>",
  "model": "claude-opus-4-7",
  "input_tokens": 12345,
  "output_tokens": 678,
  "cached_tokens": 0,
  "cost_usd": 0.0123,
  "session_id": "optional — for resumable CLI adapters",
  "session_cwd": "optional — working directory the CLI session ran in (pairs with session_id)"
}

summary is required. The engine parses bare action lines out of it (/status, /priority, /assign, /release, /request-approval, /create-issue, /blocks, /blocked-by, /relates, /write-file) and applies them; the remaining prose becomes a comment on the issue. Token counts and cost_usd are optional but feed budget tracking. As of prompt v1.2.0, a /create-issue line may be followed by an optional fenced block whose contents become the new issue's body_markdown. The result schema also accepts outbound_connections (string array, #79) — an optional audit trail of outbound hosts the agent reached during the run (e.g. from egress-proxy logs). Note: the engine currently stores remote results verbatim but does not yet act on session_id, session_cwd, or outbound_connections from a remote completion — they are reserved for future session-resume and egress-auditing support.

To report an unrecoverable failure, POST {"error":"<short message>"} to /fail instead — that records a real failure rather than waiting out timeout_seconds.

Worker setup (remote machine)

There are two ways a worker gets its key:

  • Self-registration (custom workers): call the public register endpoint at startup with no auth header, read api_key from the response, use it thereafter. This is what the operating prompt and smoke test describe.
  • Pre-provisioned (the shipped trimus-worker binary): the binary expects TRIMUS_WORKER_KEY to already be in its environment and sends it as Authorization: Bearer on every call, including its own register. An operator obtains the key from a prior register call — most easily from /admin/workers → Register worker (#335), which mints the key and generates the env file below with the server URL already filled in — and drops it in the env file.

register is idempotent: if the caller presents a worker key the server already knows, registration resumes that worker (returns the same worker_id, HTTP 200, no new api_key) instead of creating a duplicate. So the shipped binary calling register on every boot no longer leaves an orphan worker row behind. A first-time caller (no token, or an unrecognised one) still gets a brand-new worker plus a one-time api_key (HTTP 201), which the binary adopts for the rest of that session.

Prerequisites

  • Go 1.26+ (to build the binary), or a pre-built binary.
  • The tools required by local_adapter_type:
  • llm — none; the worker calls the LLM API directly.
  • claude_localclaude CLI installed and authenticated.
  • docker — Docker daemon running.

Build the worker binary

git clone https://github.com/trimus-ai/trimus.git
cd trimus
go build -o trimus-worker ./cmd/worker

Or cross-compile from your server machine:

GOOS=linux  GOARCH=amd64 go build -o trimus-worker-linux  ./cmd/worker
GOOS=darwin GOARCH=arm64 go build -o trimus-worker-darwin ./cmd/worker

Environment variables

Create an env file (e.g. ~/.config/trimus-worker/env). Source: cmd/worker/main.go.

# Required
TRIMUS_SERVER_URL=https://your-trimus-server
TRIMUS_WORKER_KEY=sk_<64 hex>            # the key issued at registration

# Optional
TRIMUS_WORKER_NAME=mac-mini-m4           # defaults to hostname
TRIMUS_WORKER_TAGS=darwin,claude_local   # comma-separated capability tags
TRIMUS_WORKER_CONCURRENCY=2              # max simultaneous tasks (default 2)
TRIMUS_WORKER_POLL_SEC=5                 # seconds between claim polls (default 5)
TRIMUS_HOME=/Users/you/.trimus           # agent home base dir (default ~/.trimus)
TRIMUS_WORKER_LLM_ALLOWED_HOSTS=llm.internal.example.com  # extra hosts the llm adapter may send the worker's env API key to
TRIMUS_WORKER_SHARED_MODELS="openai:gpt-4o,gpt-4o-mini;anthropic:claude-*"  # which models this worker will serve (#413)
LOG_LEVEL=info                           # debug | info | warn | error

Both TRIMUS_SERVER_URL and TRIMUS_WORKER_KEY are required — the binary exits with a config error if either is unset. The binary heartbeats every 30 s and polls for tasks every TRIMUS_WORKER_POLL_SEC (default 5 s).

TRIMUS_WORKER_LLM_ALLOWED_HOSTS (#170, comma-separated hostnames) matters only for the llm adapter: when an agent's adapter_config points base_url at a custom endpoint with no explicit api_key, the worker refuses to attach its environment OPENAI_API_KEY / ANTHROPIC_API_KEY unless the host is allowlisted — api.openai.com and api.anthropic.com are always allowed. Set it when a self-hosted gateway legitimately shares the instance credential.

TRIMUS_WORKER_SHARED_MODELS (#413) lets the operator declare which (provider, model) combinations this worker will serve — the worker's say over what its machine runs. Format: provider:model,model;provider:model-glob — entries separated by ;, within an entry the provider is everything up to the first : (model names may contain :, e.g. llama3:8b) and the rest is a comma-separated list of model globs (* is the only wildcard). Two effects:

  • Enforcement (authoritative): before every llm call the worker refuses a task whose (provider, model) isn't in the set — so it can never be made to run a model it didn't offer.
  • Routing (a hint): the set is advertised at registration (refreshed on every restart — it is worker-authoritative, unlike tags), and the server only routes an llm task to a worker that shares its model. A stale routing copy is harmless — the worker's enforcement is the truth.

Unset/empty = shares every model (the pre-#413 default). Only the llm adapter is filtered; claude_local / docker tasks are unaffected. The admin /admin/workers page shows each worker's advertised set (read-only).

Provider and model names are matched case-sensitively against the agent's adapter_config (providers are the lowercase family names — openai, anthropic, openai_compatible). If a worker never claims the llm tasks you expect, check the casing and exact model names first — a mismatch simply leaves the tasks for another worker (the claim returns "nothing to do", not an error).

Run the worker

set -a; source ~/.config/trimus-worker/env; set +a
./trimus-worker

set -a marks the sourced assignments for export so the trimus-worker child process inherits them; a bare source leaves them shell-local and the binary would exit with TRIMUS_SERVER_URL is required. The service units below read the same file directly (launchd via EnvironmentVariables, systemd via EnvironmentFile=) and need no set -a.

The worker logs structured JSON to stdout:

{"time":"2026-05-30T10:00:00Z","level":"INFO","msg":"worker registered","worker_id":"550e8400..."}
{"time":"2026-05-30T10:00:05Z","level":"INFO","msg":"executing task","task_id":"...","agent_id":"..."}
{"time":"2026-05-30T10:02:13Z","level":"INFO","msg":"task complete","model":"...","input_tokens":1240}

The worker can install itself as a managed service — starting on boot/login and restarting on failure — so you don't hand-copy the unit files below:

# per-user login service (default; macOS launchd or Linux systemd --user)
trimus-worker service install --env-file ~/.config/trimus-worker/env

# system-wide boot service (Linux/systemd only; needs root)
sudo trimus-worker service install --system --env-file /etc/trimus-worker/env

trimus-worker service status      # is it loaded / running?
trimus-worker service uninstall   # stop + disable + remove the unit
  • Scope: --user (default) runs as you, under $HOME, and is required for the claude_local / codex_local adapters to reach ~/.claude. --system is a root-owned boot service and is Linux only (on macOS the worker is a per-user LaunchAgent — omit --system).
  • Boot survival (Linux --user): install runs loginctl enable-linger for you, so the service starts at boot even when you're not logged in — the headless remote-worker case. Undo with loginctl disable-linger <user>.
  • Idempotent: re-run install after upgrading the binary or editing the env file; it rewrites the unit and restarts the worker so it picks up the change. (If your binary lives behind a package-manager symlink that moved on upgrade — e.g. Homebrew — re-run install so the unit points at the new path.)
  • --dry-run prints the exact unit and commands without touching anything — useful to audit first, or on a box where you register the service by hand.
  • The env file is sourced by the service; the worker exits without TRIMUS_SERVER_URL + TRIMUS_WORKER_KEY, so create it before (or right after) installing.

The generated units are modeled on the canonical templates in scripts/service/; the manual instructions below remain for when you'd rather write the unit yourself.

Run as a service by hand (macOS launchd)

~/Library/LaunchAgents/com.trimus.worker.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>          <string>com.trimus.worker</string>
  <key>ProgramArguments</key>
  <array><string>/usr/local/bin/trimus-worker</string></array>
  <key>EnvironmentVariables</key>
  <dict>
    <key>TRIMUS_SERVER_URL</key>  <string>https://your-trimus-server</string>
    <key>TRIMUS_WORKER_KEY</key>  <string>sk_...</string>
    <key>TRIMUS_WORKER_TAGS</key> <string>darwin,claude_local</string>
    <key>TRIMUS_HOME</key>        <string>/Users/you/.trimus</string>
  </dict>
  <key>RunAtLoad</key>      <true/>
  <key>KeepAlive</key>      <true/>
  <key>StandardOutPath</key> <string>/tmp/trimus-worker.log</string>
  <key>StandardErrorPath</key><string>/tmp/trimus-worker.log</string>
</dict>
</plist>
launchctl load ~/Library/LaunchAgents/com.trimus.worker.plist

Run as a service by hand (Linux systemd)

/etc/systemd/system/trimus-worker.service:

[Unit]
Description=Trimus Remote Worker
After=network.target

[Service]
Type=simple
User=trimus
EnvironmentFile=/etc/trimus-worker/env
ExecStart=/usr/local/bin/trimus-worker
Restart=on-failure
RestartSec=10

[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable --now trimus-worker

The repo also ships trimus-worker.service (system, hardened, llm-only) and trimus-worker.user.service (user, local-CLI adapters), installed disabled until you opt in.

Hardware targeting vs load balancing

Load balancing (identical machines)

Leave tags empty in the agent's adapter_config and at the project/company tiers (see below). Any available worker then claims the task; the worker that polls soonest after the task appears wins.

Hardware targeting

Use tags to route specific agents to specific machines. A worker can claim a task only when the task's required tags are a subset of the worker's tags. A worker with ["darwin","claude_local","high-memory"] can claim tasks requiring ["darwin"], ["claude_local"], or ["darwin","claude_local"] — but not one requiring a tag it lacks.

Worker affinity defaults (#261)

Empty agent tags don't immediately mean "any worker". The engine resolves an inheritance chain once, server-side, at dispatch: the agent's explicit adapter_config.tags > the project's default_worker_tags > the company's default_worker_tags. The first non-empty tier wins outright (replace, not merge). Only when all three tiers are empty can any worker claim the task. A tier-lookup failure (project or company row unreadable) fails the dispatch rather than silently degrading to empty tags — operators use tags to pin sensitive agents to specific hosts, so quietly widening the claim set to "any worker" would defeat routing isolation.

Changing a worker's tags (#336)

A worker's tags are server-authoritative, and an instance admin can edit them at any time from /admin/workers — click edit in the worker's Tags cell, adjust the comma-separated list, and Save. The change is stored on the worker row and takes effect on the worker's next claim poll — no worker restart, and nothing to change on the worker host.

Because the server's stored set wins, two things follow:

  • The env file's TRIMUS_WORKER_TAGS matters only at first registration. After a worker exists, editing its env value and restarting does not change its tags — the idempotent register-resume path keeps the stored set. Update the env file anyway to keep it honest, but a stale value can no longer cause wrong matching. When a resuming worker presents env tags that differ from the stored set, the server logs the drift and echoes the authoritative tags in the register response so the worker can reconcile.
  • A claim-body tags set can only narrow, never broaden. The effective claim set is the intersection of the request's tags with the worker's stored tags, so a worker may self-limit to a subset but can never claim work outside the tags it's registered for. Custom workers that previously relied on the claim body to replace their tags must instead be retagged on /admin/workers.

Listing registered workers

curl -s -H "Authorization: Bearer $ADMIN_KEY" \
  https://your-trimus-server/api/workers

(Uses an admin session/bearer key, not the worker key.) A worker that stops heartbeating is considered stale: the operating contract documents a 5-minute no-heartbeat window after which a worker is treated as offline and skipped by the dispatcher until it heartbeats again.

Removing or disabling a worker (admin UI)

The /admin/workers page (instance-admin only) lists every registered worker with Register worker (#335, see §2), edit tags (#336, see Changing a worker's tags), Disable / Enable, Drain / Undrain, and Delete actions:

  • edit (in the Tags cell) replaces the worker's capability tags; the stored set is authoritative and the change applies on the next claim (#336).
  • Disable sets a disabled flag and WorkerAuth then rejects that worker's key with 403 until you re-enable it. The registration row and its history are kept — use this to fence a worker temporarily without re-provisioning.
  • Drain (#77) marks the worker as draining: its key stays valid and in-flight tasks finish, but the claim query stops offering it new tasks until you Undrain. Use Drain → wait for in-flight work to finish → Delete for graceful decommissioning.
  • Delete permanently removes the worker and its API key (remote_worker_keys cascade); any remote_tasks it had claimed keep their history with worker_id nulled.

Troubleshooting

Worker registers but never claims tasks - Verify the agent's adapter_type is exactly "remote". - Check the worker's tags are a superset of the agent's adapter_config.tags. - Confirm TRIMUS_SERVER_URL has no trailing slash and the worker can reach it.

Tasks time out on the server - Increase timeout_seconds in the agent's adapter_config. - Check worker logs for execution errors. - Default is 600 s. For long claude_local runs, 1200–1800 s may fit better.

claude_local fails on the worker - Ensure claude is installed and authenticated (which claude, claude --version). - Ensure TRIMUS_HOME is writable.

Uneven distribution across workers - Reduce TRIMUS_WORKER_POLL_SEC so faster machines pick up the next task sooner. - Increase TRIMUS_WORKER_CONCURRENCY on machines with more resources.

See also