challenge-troubleshoot — Symptom → cause → next command
When the user reports a problem, do this in order:
- Reproduce / verify the symptom with a read-only call (
/jobs, /result, /log, /current-user-info) before doing anything destructive. For "what's the status" questions, /jobs is the only endpoint that carries one.
- Match the symptom to the table below.
- Hand the user the next command from the Action column. Do NOT auto-resubmit jobs (quota cost) or auto-kill agents (loses in-flight cases) without explicit confirmation.
Diagnostic heuristic: 4xx is never a network blip
A structured {"status":"error", ...} body with HTTP 4xx (400/401/403/404) is a semantic rejection by the platform — the request reached the server and was refused on its merits. Retrying it without changing the request gets the same answer and, for POST /api/challenge/job, burns a daily submission slot each time.
Only these qualify as transient and may be retried with backoff:
- TCP connection reset / timeout / DNS failure (curl exits non-zero before getting a response)
- HTTP
5xx (server-side, signals a transient backend failure)
If the user says "可能是网络抖动 / maybe a network blip" but you have a 4xx body in hand, push back: name the actual error and point at the table row.
Diagnostic heuristic: 5xx is often a missing / wrong token, not a real platform outage
curl -fsS collapses any HTTP 5xx to a terse (22) The requested URL returned error: 5XX and hides the body. Before concluding the platform is down, check this in order:
- State file sourced? Each Bash call should start with
[ -f ~/.simubotix-challenge.env ] && . ~/.simubotix-challenge.env — AI assistants spawn every command in a new subshell, so plain export from a previous call is gone.
- Token presence after sourcing.
echo "len=${#CHALLENGE_TOKEN}" — if it's 0, either the file doesn't exist (re-run challenge-login Step 1) or the file exists but doesn't contain the key (the previous login silently failed — see challenge-login Step 1's status check).
- Token shape. Should be a 3-segment JWT (two dots).
echo "$CHALLENGE_TOKEN" | head -c 20; echo — gibberish or null means a previous step persisted a bad value.
- Body of the actual response. Drop
-f so curl prints the body even on non-2xx:[ -f ~/.simubotix-challenge.env ] && . ~/.simubotix-challenge.env
curl -sS -i "$BASE_URL/api/challenge/current-user-info" \
-H "Authorization: Bearer $CHALLENGE_TOKEN" | tail -15
The body usually says exactly what's wrong (often a 401-style message returned with a 500 status code).
Only after the token is confirmed valid (e.g. the same value just succeeded against /login) should you treat the 5xx as a real platform issue and retry with backoff.
Symptom table
| # |
Symptom |
Likely cause |
Action |
| 1 |
POST /api/challenge/job returns HTTP 400 invalid board / no task templates for board |
config.board is missing or not in the currently-supported set |
Today the supported values are instruction (default), spatial, manip, robust. Use one of those and resubmit. Do not retry the same wrong board — 400 is a semantic rejection, retrying burns a daily submission slot each time. This holds even when the user says "maybe a network blip" — see the heuristic above. If a contestant insists a new board exists, verify with the organizers first; challenge-submit-job Step 2 is the only list to trust. |
| 2 |
POST /api/challenge/job returns an "upload limit" error / /api/challenge/submission/quota returns remaining: 0 |
Today's submission quota is used up |
Wait until Beijing midnight (UTC+8), or work with an existing job. Report the actual used / limit from the endpoint — don't quote a cap from memory. |
| 3 |
Any plain HTTP /api/challenge/* call (login, result, log, job, tunnel-endpoint) returns 401 |
Invalid / expired CHALLENGE_TOKEN |
challenge-login Step 3 to refresh; if refresh also 401s, fall back to Step 1 (email + password). This only affects the plain HTTP APIs — it does not touch a JOB_TOKEN-based tunnel dial. |
| 3b |
The WS handshake (GET /api/challenge/tunnel) returns 401 |
When dialing with JOB_TOKEN (the default per challenge-run-agent), the handshake does not 401 on token expiry — job_tokens don't expire. A 401 here means the ?job= UUID is not owned by this account, the job is in a terminal state, or the ?job= doesn't match the job the token was issued for. If still dialing with CHALLENGE_TOKEN (no JOB_TOKEN available, e.g. an older platform build), a 401 can additionally mean the login token itself expired |
Verify JOB_UUID / ?job= came from this account's POST /api/challenge/job and that the job isn't terminal. Only if dialing with CHALLENGE_TOKEN, also try challenge-login Step 3 to refresh. |
| 4 |
Agent connects, then immediately disconnects |
Per-user parallelism cap exceeded — too many agents online |
Reduce K in challenge-run-agent, or wait for older agents to finish. |
| 5 |
Job stuck at detailed_status: pending / inference_disconnected, or progress not advancing for minutes |
No agent online, or all agents dropped past the 30 s window |
ps/check the agent processes; relaunch via challenge-run-agent if needed. queued is different — it means the cluster has no free GPU; the agent is fine and you must keep it online. |
| 6 |
Agent disconnected unexpectedly mid-run |
Network blip |
Within the gateway-side 30 s reconnect window, tunnel.sh redials automatically with the same agent_id to resume the open session. Do not manually relaunch in that window — you'd race the automatic redial and the gateway will reject the duplicate. Past 30 s the open session is gone; resubmitting a new job costs a daily submission slot — confirm before suggesting it. |
| 7 |
Received drain control frame |
Platform finished dispatching cases and is asking for graceful shutdown |
Let in-flight sessions finish, close the socket, and do NOT reconnect. |
| 8 |
Job ends at detailed_status: failed (list status = 4) |
A case exhausted its retries, which kills the whole job |
GET /api/challenge/job/$JOB_ID/log (see challenge-poll-result). Read failure_reason before any log text: 5/6/7/8 are platform-side teardowns, not your bug — 8 in particular means a different case was the root cause. 9 (tunnel lost) is the opposite — it usually means your agent dropped the WebSocket; check result.error for the sub-reason and see rows #5/#6 above. For 1/3/4, fetch result.stdout (usually a presigned URL, not inline text) and read the traceback. Common causes: handler exception during warmup (row #11), OOM, action-encoding mismatch. Fix the bug locally before resubmitting — resubmission costs a daily submission slot. |
| 9 |
/api/challenge/tunnel/endpoint returns empty string |
Gateway not yet ready |
Fall back to the fixed default ws://120.92.88.78/api/challenge/tunnel; the endpoint may simply be omitted because the host never changes. |
| 10 |
parallelism in the job response is 0 |
User's MaxConcurrentCases is misconfigured (not "exhausted" — exhaustion is a runtime gateway check, not a response field) |
Stop and surface to organizers; do not launch agents. |
| 11 |
Agent stuck in WARMUP, never reaches RUNNING — or the job ends Failed with a traceback referencing frame_bytes=b'' / zero-length decode in the /log output |
Inference handler errored on the warmup empty frame |
The warmup call passes an empty frame — handlers must tolerate that (e.g. short-circuit when len(frame_bytes) == 0 and return a no-op action). Fix in the inference repo, then resubmit (mind quota). |
| 12 |
tunnel.sh exits with code 1 immediately |
Bad arguments / handler import failure / reconnect retries exhausted |
Re-check <gpu_index> <job_uuid> <gateway_url> argument order; check inference-repo handler import path. |
| 13 |
tunnel.sh exits with code 130 |
Ctrl-C |
Expected; user-driven. |
| 14 |
POST /api/challenge/job returns HTTP 400 paper_link is not a valid URL / paper_link too long |
Optional paper_link field is malformed or > 512 chars |
Fix the URL or omit the field entirely; resubmit. Consumes a submission slot only if the call gets past validation. |
Diagnostics shortlist
Run these in order when triaging an unclear failure:
# 1. Token still valid?
curl -fsS "$BASE_URL/api/challenge/current-user-info" \
-H "Authorization: Bearer $CHALLENGE_TOKEN" | jq
# 2. Job exists, what status? (status lives on the LIST — /result has no status field)
curl -fsS "$BASE_URL/api/challenge/jobs?page=1&per-page=100" \
-H "Authorization: Bearer $CHALLENGE_TOKEN" \
| jq -c --argjson id "$JOB_ID" '.items[] | select(.id == $id)
| {id, status, detailed_status, progress, score}'
# 3. If detailed_status == failed, get the log (read failure_reason first)
curl -fsS "$BASE_URL/api/challenge/job/$JOB_ID/log" \
-H "Authorization: Bearer $CHALLENGE_TOKEN" | jq
# 4. Scores, once terminal
curl -fsS "$BASE_URL/api/challenge/job/$JOB_ID/result" \
-H "Authorization: Bearer $CHALLENGE_TOKEN" | jq
# 5. Gateway endpoint sane?
curl -fsS "$BASE_URL/api/challenge/tunnel/endpoint" \
-H "Authorization: Bearer $CHALLENGE_TOKEN"
$JOB_ID must be the numeric id; the UUID belongs only to the tunnel dial. Passing a UUID
to these paths yields 400 invalid job_id, which curl -fsS hides behind a bare exit code 22.
Things NOT to do
- The website/API domain is
robocoliseum.ai, but the gateway host is fixed at 120.92.88.78 and is a separate host: BASE_URL=https://robocoliseum.ai, TUNNEL_ENDPOINT=ws://120.92.88.78/api/challenge/tunnel. Do not "fix" the tunnel URL by swapping in the website domain. Prefer a tunnel_endpoint from the job response if present, but the fixed default is a safe fallback.
- Do not reconnect after a
drain — the platform is finishing up, and the connection will be rejected.
- Do not spam
POST /api/challenge/job to "retry" — each call consumes one of the day's submission slots. Remaining quota is not authorization to guess (e.g. guessing a board because "quota 还有"). Skill rules forbid the guess itself, independent of remaining slots.
- Do not state the daily cap from memory. Read
GET /api/challenge/submission/quota and quote its limit / used / remaining; the limit is a platform setting and has changed before.
- Do not kill running agents to "free a slot" without checking the job list first; you may be killing the agent that's about to finish your last case.
- Do not retry a 4xx as if it were a network blip — see the diagnostic heuristic at the top of this file.
- Do not poll
/result for status — it returns only {tasks, total}. A loop that branches on .status there never terminates. Use detailed_status from GET /api/challenge/jobs.
Reference
For the WS wire protocol (dial, control frames, binary frame layout, state machine, reconnect,
heartbeat), see the appendix of challenge-run-agent — it is inlined there in full. Most
contestants don't need it; ./scripts/tunnel.sh from the inference repo already implements it.
For the observation/action payload schema inside the data frames, see
challenge-inference-protocol.
1---2name: challenge-troubleshoot3description: Use when something in the Simulation Challenge pipeline is misbehaving — auth errors, agent disconnects, jobs stuck in Pending, jobs ending in Failed, drain frames. Maps a symptom to its likely cause and the next command to run.4---56# challenge-troubleshoot — Symptom → cause → next command78When the user reports a problem, do this in order:9101. **Reproduce / verify the symptom** with a read-only call (`/jobs`, `/result`, `/log`, `/current-user-info`) before doing anything destructive. For "what's the status" questions, `/jobs` is the only endpoint that carries one.112. Match the symptom to the table below.123. Hand the user the next command from the **Action** column. Do NOT auto-resubmit jobs (quota cost) or auto-kill agents (loses in-flight cases) without explicit confirmation.1314## Diagnostic heuristic: 4xx is never a network blip1516A structured `{"status":"error", ...}` body with HTTP **4xx** (400/401/403/404) is a **semantic rejection** by the platform — the request reached the server and was refused on its merits. Retrying it without changing the request gets the same answer and, for `POST /api/challenge/job`, **burns a daily submission slot each time**.1718Only these qualify as transient and may be retried with backoff:1920- TCP connection reset / timeout / DNS failure (curl exits non-zero before getting a response)21- HTTP `5xx` (server-side, signals a transient backend failure)2223If the user says "可能是网络抖动 / maybe a network blip" but you have a 4xx body in hand, push back: name the actual error and point at the table row.2425## Diagnostic heuristic: 5xx is often a missing / wrong token, not a real platform outage2627`curl -fsS` collapses any HTTP 5xx to a terse `(22) The requested URL returned error: 5XX` and hides the body. Before concluding the platform is down, check this in order:28291. **State file sourced?** Each Bash call should start with `[ -f ~/.simubotix-challenge.env ] && . ~/.simubotix-challenge.env` — AI assistants spawn every command in a new subshell, so plain `export` from a previous call is gone.302. **Token presence after sourcing.** `echo "len=${#CHALLENGE_TOKEN}"` — if it's 0, either the file doesn't exist (re-run `challenge-login` Step 1) or the file exists but doesn't contain the key (the previous login silently failed — see `challenge-login` Step 1's status check).313. **Token shape.** Should be a 3-segment JWT (two dots). `echo "$CHALLENGE_TOKEN" | head -c 20; echo` — gibberish or `null` means a previous step persisted a bad value.324. **Body of the actual response.** Drop `-f` so curl prints the body even on non-2xx:33 ```bash34 [ -f ~/.simubotix-challenge.env ] && . ~/.simubotix-challenge.env35 curl -sS -i "$BASE_URL/api/challenge/current-user-info" \36 -H "Authorization: Bearer $CHALLENGE_TOKEN" | tail -1537 ```38 The body usually says exactly what's wrong (often a 401-style message returned with a 500 status code).3940Only after the token is confirmed valid (e.g. the same value just succeeded against `/login`) should you treat the 5xx as a real platform issue and retry with backoff.4142## Symptom table4344| # | Symptom | Likely cause | Action |45|---|---------|--------------|--------|46| 1 | `POST /api/challenge/job` returns **HTTP 400** `invalid board` / `no task templates for board` | `config.board` is missing or not in the currently-supported set | Today the supported values are `instruction` (default), `spatial`, `manip`, `robust`. Use one of those and resubmit. **Do not retry the same wrong board** — 400 is a semantic rejection, retrying burns a daily submission slot each time. This holds even when the user says "maybe a network blip" — see the heuristic above. If a contestant insists a new board exists, verify with the organizers first; `challenge-submit-job` Step 2 is the only list to trust. |47| 2 | `POST /api/challenge/job` returns an "upload limit" error / `/api/challenge/submission/quota` returns `remaining: 0` | Today's submission quota is used up | Wait until Beijing midnight (UTC+8), or work with an existing job. Report the actual `used` / `limit` from the endpoint — don't quote a cap from memory. |48| 3 | Any plain HTTP `/api/challenge/*` call (login, result, log, job, tunnel-endpoint) returns `401` | Invalid / expired `CHALLENGE_TOKEN` | `challenge-login` Step 3 to refresh; if refresh also 401s, fall back to Step 1 (email + password). This only affects the plain HTTP APIs — it does not touch a `JOB_TOKEN`-based tunnel dial. |49| 3b | The WS handshake (`GET /api/challenge/tunnel`) returns `401` | When dialing with `JOB_TOKEN` (the default per `challenge-run-agent`), the handshake does **not** 401 on token expiry — `job_token`s don't expire. A `401` here means the `?job=` UUID is not owned by this account, the job is in a terminal state, **or** the `?job=` doesn't match the job the token was issued for. If still dialing with `CHALLENGE_TOKEN` (no `JOB_TOKEN` available, e.g. an older platform build), a `401` can additionally mean the login token itself expired | Verify `JOB_UUID` / `?job=` came from this account's `POST /api/challenge/job` and that the job isn't terminal. Only if dialing with `CHALLENGE_TOKEN`, also try `challenge-login` Step 3 to refresh. |50| 4 | Agent connects, then immediately disconnects | Per-user `parallelism` cap exceeded — too many agents online | Reduce K in `challenge-run-agent`, or wait for older agents to finish. |51| 5 | Job stuck at `detailed_status: pending` / `inference_disconnected`, or `progress` not advancing for minutes | No agent online, or all agents dropped past the 30 s window | `ps`/check the agent processes; relaunch via `challenge-run-agent` if needed. **`queued` is different** — it means the cluster has no free GPU; the agent is fine and you must keep it online. |52| 6 | Agent disconnected unexpectedly mid-run | Network blip | Within the gateway-side **30 s reconnect window**, `tunnel.sh` redials **automatically** with the same `agent_id` to resume the open session. **Do not manually relaunch** in that window — you'd race the automatic redial and the gateway will reject the duplicate. Past 30 s the open session is gone; resubmitting a new job costs a daily submission slot — confirm before suggesting it. |53| 7 | Received `drain` control frame | Platform finished dispatching cases and is asking for graceful shutdown | Let in-flight sessions finish, close the socket, and **do NOT reconnect**. |54| 8 | Job ends at `detailed_status: failed` (list `status` = `4`) | A case exhausted its retries, which kills the whole job | `GET /api/challenge/job/$JOB_ID/log` (see `challenge-poll-result`). **Read `failure_reason` before any log text**: `5`/`6`/`7`/`8` are platform-side teardowns, not your bug — `8` in particular means a *different* case was the root cause. `9` (tunnel lost) is the opposite — it usually means your agent dropped the WebSocket; check `result.error` for the sub-reason and see rows #5/#6 above. For `1`/`3`/`4`, fetch `result.stdout` (usually a presigned URL, not inline text) and read the traceback. Common causes: handler exception during warmup (row #11), OOM, action-encoding mismatch. **Fix the bug locally before resubmitting** — resubmission costs a daily submission slot. |55| 9 | `/api/challenge/tunnel/endpoint` returns empty string | Gateway not yet ready | Fall back to the fixed default `ws://120.92.88.78/api/challenge/tunnel`; the endpoint may simply be omitted because the host never changes. |56| 10 | `parallelism` in the job response is `0` | User's `MaxConcurrentCases` is misconfigured (not "exhausted" — exhaustion is a runtime gateway check, not a response field) | Stop and surface to organizers; do not launch agents. |57| 11 | Agent stuck in `WARMUP`, never reaches `RUNNING` — **or** the job ends `Failed` with a traceback referencing `frame_bytes=b''` / zero-length decode in the `/log` output | Inference handler errored on the warmup empty frame | The warmup call passes an **empty frame** — handlers must tolerate that (e.g. short-circuit when `len(frame_bytes) == 0` and return a no-op action). Fix in the inference repo, then resubmit (mind quota). |58| 12 | `tunnel.sh` exits with code 1 immediately | Bad arguments / handler import failure / reconnect retries exhausted | Re-check `<gpu_index> <job_uuid> <gateway_url>` argument order; check inference-repo handler import path. |59| 13 | `tunnel.sh` exits with code 130 | Ctrl-C | Expected; user-driven. |60| 14 | `POST /api/challenge/job` returns **HTTP 400** `paper_link is not a valid URL` / `paper_link too long` | Optional `paper_link` field is malformed or > 512 chars | Fix the URL or omit the field entirely; resubmit. Consumes a submission slot only if the call gets past validation. |6162## Diagnostics shortlist6364Run these in order when triaging an unclear failure:6566```bash67# 1. Token still valid?68curl -fsS "$BASE_URL/api/challenge/current-user-info" \69 -H "Authorization: Bearer $CHALLENGE_TOKEN" | jq7071# 2. Job exists, what status? (status lives on the LIST — /result has no status field)72curl -fsS "$BASE_URL/api/challenge/jobs?page=1&per-page=100" \73 -H "Authorization: Bearer $CHALLENGE_TOKEN" \74 | jq -c --argjson id "$JOB_ID" '.items[] | select(.id == $id)75 | {id, status, detailed_status, progress, score}'7677# 3. If detailed_status == failed, get the log (read failure_reason first)78curl -fsS "$BASE_URL/api/challenge/job/$JOB_ID/log" \79 -H "Authorization: Bearer $CHALLENGE_TOKEN" | jq8081# 4. Scores, once terminal82curl -fsS "$BASE_URL/api/challenge/job/$JOB_ID/result" \83 -H "Authorization: Bearer $CHALLENGE_TOKEN" | jq8485# 5. Gateway endpoint sane?86curl -fsS "$BASE_URL/api/challenge/tunnel/endpoint" \87 -H "Authorization: Bearer $CHALLENGE_TOKEN"88```8990`$JOB_ID` must be the **numeric** `id`; the UUID belongs only to the tunnel dial. Passing a UUID91to these paths yields `400 invalid job_id`, which `curl -fsS` hides behind a bare exit code 22.9293## Things NOT to do9495- The website/API domain is `robocoliseum.ai`, but the gateway host is **fixed at `120.92.88.78`** and is a separate host: `BASE_URL=https://robocoliseum.ai`, `TUNNEL_ENDPOINT=ws://120.92.88.78/api/challenge/tunnel`. **Do not** "fix" the tunnel URL by swapping in the website domain. Prefer a `tunnel_endpoint` from the job response if present, but the fixed default is a safe fallback.96- **Do not** reconnect after a `drain` — the platform is finishing up, and the connection will be rejected.97- **Do not** spam `POST /api/challenge/job` to "retry" — each call consumes one of the day's submission slots. **Remaining quota is not authorization to guess** (e.g. guessing a `board` because "quota 还有"). Skill rules forbid the guess itself, independent of remaining slots.98- **Do not** state the daily cap from memory. Read `GET /api/challenge/submission/quota` and quote its `limit` / `used` / `remaining`; the limit is a platform setting and has changed before.99- **Do not** kill running agents to "free a slot" without checking the job list first; you may be killing the agent that's about to finish your last case.100- **Do not** retry a 4xx as if it were a network blip — see the diagnostic heuristic at the top of this file.101- **Do not** poll `/result` for status — it returns only `{tasks, total}`. A loop that branches on `.status` there never terminates. Use `detailed_status` from `GET /api/challenge/jobs`.102103## Reference104105For the WS wire protocol (dial, control frames, binary frame layout, state machine, reconnect,106heartbeat), see the **appendix of `challenge-run-agent`** — it is inlined there in full. Most107contestants don't need it; `./scripts/tunnel.sh` from the inference repo already implements it.108For the observation/action payload schema inside the data frames, see109`challenge-inference-protocol`.