This patch covers behavior fixes and one new action since the last agent-facing gist. Read this alongside the existing gists (full API spec, mutations flow, sprite.lookup additions). Nothing here renames or removes any existing endpoint or arg; the patch is additive.
probe alone fails when the sprite is cold — there's no channel to send
the manual probe to, so the action returns {:error, :not_connected}.
The common pattern (wake → confirm health) is now one approval.
| field | value |
|---|---|
| name | start_and_probe |
| destructive | false (no typed-name confirm prompt) |
| description | "Wake a cold sprite and, once its channel reconnects, dispatch a manual health probe." |
wait_ms is the budget for the channel-reconnect poll after start
succeeds. Polled every 500ms internally.
Same POST /internal/spritesdoc/propose flow as every other action:
curl -X POST "$SPRITES_API/internal/spritesdoc/propose" \
-H 'authorization: Bearer …' \
-H 'content-type: application/json' \
-d '{
"action": "start_and_probe",
"target": { "org_id": 77, "sprite_name": "demo-1" },
"args": { "wait_ms": 15000 },
"reason": "Diagnosing cold-start regression"
}'Response is the standard proposal envelope with approval_url. The
operator clicks Authorize in the popup. No typed-name confirmation
because the action is non-destructive.
The agent polls GET /internal/spritesdoc/proposals/<id> until status
is terminal. On executed, the result field is one of:
// happy path: machine started AND channel reconnected in time
{
"machine_id": "…",
"channel_pid": "<pid>",
"message": "machine started and health probe dispatched"
}
// partial: machine started, but the channel didn't reconnect within
// wait_ms. The result is still {:ok}, NOT a failure — the agent should
// poll sprite.lookup until the channel is up, then call `probe` again.
{
"machine_id": "…",
"probe_dispatched": false,
"message": "machine started; channel did not reconnect within 15000ms — probe skipped"
}A hard start failure (Fly API error, etc.) still surfaces as
status: "failed" with an error string — same as the existing
start action.
| Sprite state | Use |
|---|---|
Channel currently connected (runtime_status: running / warm) |
probe — fast, no Fly API call |
Cold (runtime_status: cold / unknown and no channel) |
start_and_probe — wake then probe |
| You only need to wake (don't care about probe right now) | start |
If your agent didn't have start in its action vocabulary, it does now
(it's been there since the mutations branch first shipped — just
flagging in case the local docs were stale).
{
"action": "start",
"target": { "org_id": 77, "sprite_name": "demo-1" },
"args": {}
}Result: { "machine_id": "…", "message": "machine started or already running" }.
Non-destructive, no confirmation prompt.
These are bugs that produced spurious 500s or wrong field values. No calling code needs to change; just expect more useful responses.
sprite.lookup and evidence-pack previously returned 500 when the
target sprite didn't exist. They now return:
HTTP/1.1 404 Not Found
content-type: application/json
{ "query": "sprite.lookup", "error": "sprite_not_found" }Same change for org_not_found. If the agent's retry policy treats 5xx
as "transient, retry" and 4xx as "permanent, surface to user," that
classification is now correct.
Two paths in sprite.lookup and org.sprite_list could return
runtime_status: null when the sprite was cold and its DB
runtime_state column was nil. Root cause: is_atom(nil) is true in
Elixir, so a normalize_runtime/1 clause matching atoms was returning
nil instead of falling through to :unknown.
After the fix, runtime_status is always one of:
"running", "warm", "cold", "error", "unknown".
If the agent had defensive code treating null as "unknown," that's
fine to leave in place — it just won't fire anymore.
The machine list was raising
Fly.Machines.API.Machine.fetch/2 is undefined because the code was
using machine["id"] map-access on a struct. Fixed to use struct field
access. The shape of each machine entry is now:
{
"id": "…",
"app": "…",
"state": "started" | "stopped" | …,
"region": "iad",
"version": "…",
"image_ref": "registry.fly.io/…"
}This is slimmer than the previous documented shape because the Fly
API client only maps a subset of fields onto its struct. name,
private_ip, instance_id, created_at, updated_at are no longer
returned (they were never reliably populated anyway — the previous
shape was aspirational). If your agent prompt referenced any of those,
remove them.
POST /internal/spritesdoc/evidence-pack previously only worked with
{ "target": { "org_id": …, "sprite_name": "…" } }. A target shaped
as { "target": { "sprite_id": "sprite-…" } } would FunctionClauseError
under the hood and return Phoenix's bare 500 page.
Both shapes now work, matching sprite.lookup. The endpoint also now
returns the standard SpritesDoc error envelope on any unexpected crash:
{ "error": "internal_error", "detail": "<exception message>" }After this patch, the action registry has:
| name | destructive | confirm? | summary |
|---|---|---|---|
probe |
no | — | Manual health probe; requires channel up. |
start |
no | — | Wake a cold sprite. |
start_and_probe |
no | — | Wake then probe (single approval). |
upgrade |
yes | typed sprite name | Run upgrade to a target sprite-env version. |
recover |
yes | typed sprite name | Destroy and reprovision the Fly machine; data preserved via Litestream. |
There is no runtime-discovery endpoint for actions today (the existing
/catalog only lists queries). The action vocabulary is what's in
this table — check the "what's new" gists for additions before assuming
an action exists.
When the agent has decided "I want to wake sprite X and confirm it's healthy":
- Call
sprite.lookupfirst. Don't propose anything if it's alreadyruntime_status: running. - If
cold/unknown: proposestart_and_probe. Wait for the operator to authorize. - If
start_and_probereturns withprobe_dispatched: false, pollsprite.lookupuntilruntime_statusflips torunningorwarm. Then propose a follow-upprobe. - Surface the final health result (from
sprite.lookupafter probe) to the user.
Do not propose recover automatically. recover is destructive
and should only be proposed when the diagnosis explicitly indicates the
machine is broken (e.g., repeatedly fails to start, lease stuck,
Litestream lag suggests data corruption that a fresh provision will
clear). The operator will see the typed-confirm prompt regardless.
{ "wait_ms": 15000 // optional, integer 1000–30000, default 15000 }