# Signals

A unit talks to the platform through a small agent that runs beside it and shares the directory `/zaroz`. There is **no network port**: you write files, or call a Unix socket.

## What a unit can signal (contract v1, a closed set)

| Signal | Meaning |
|---|---|
| `ready` | I have loaded; send me traffic. Needed only with `readiness: signal` |
| `accepting` `true` or `false` | in or out of the load-balanced set while staying healthy. `false` removes the unit from traffic and from allocation within about a second |
| `idle`, `busy` | what the unit is doing. Drives `idle_action` and the rollout and scale-down choices. Only transitions are recorded |
| `shutdown` | I am done. Same as exiting with code 0 |

Unknown names are ignored and logged by the agent, never fatal.

## By file

```bash
echo > /zaroz/signals/ready
echo false > /zaroz/signals/accepting     # full: stop sending players
echo true  > /zaroz/signals/accepting
echo > /zaroz/signals/idle
echo > /zaroz/signals/busy
echo > /zaroz/signals/shutdown
```

The agent consumes each file as it appears. An empty `accepting` file means `true`.

## By socket

HTTP/1.1 over the Unix socket `/zaroz/signal.sock`:

```bash
curl --unix-socket /zaroz/signal.sock -X POST http://unit/ready
curl --unix-socket /zaroz/signal.sock -X POST http://unit/accepting -d false
curl --unix-socket /zaroz/signal.sock http://unit/unit
curl --unix-socket /zaroz/signal.sock http://unit/user-data
curl --unix-socket /zaroz/signal.sock "http://unit/allocation?wait=60"   # long-poll up to 60 s
curl --unix-socket /zaroz/signal.sock "http://unit/state?wait=60"        # returns when draining starts
```

## What a unit receives

| File | Content |
|---|---|
| `/zaroz/unit.json` | key, labels (updated live), origin, workload, private name, public endpoints |
| `/zaroz/user-data.json` | your `user_data`, or `null` |
| `/zaroz/allocation.json` | `null` until allocated, then `{id, allocated_at, generation}` |
| `/zaroz/state.json` | `{draining: false}` or `{draining: true, reason: "stop" \| "destroy" \| "replace" \| "scale_down"}` |

Every file is written atomically (a temporary name, then a rename), so a reader never sees half a file. Read them again when `allocation.json` or `state.json` changes; the socket's `?wait=` long-polls save the polling.

## A minimal unit

```ts
import { watch, readFileSync, writeFileSync } from "node:fs";

const dir = process.env.ZAROZ_DIR ?? "/zaroz";
const read = (f: string) => JSON.parse(readFileSync(`${dir}/${f}`, "utf8"));
const signal = (name: string, body = "") => writeFileSync(`${dir}/signals/${name}`, body);

await startMyServer();                 // load the map, open the port
signal("ready");

watch(`${dir}/allocation.json`, () => {
  const allocation = read("allocation.json");
  if (allocation) startMatch(read("user-data.json"));   // the roster arrives here
});

watch(`${dir}/state.json`, () => {
  if (read("state.json").draining) {
    signal("accepting", "false");       // stop taking new players
    finishMatchThenExit();              // exit 0 when done
  }
});
```

## Where signals go

The agent writes them to a per-unit object in the cluster; the platform watches it. Nothing billed depends on a signal, so a program that lies about its signals can only stop, idle-destroy or mark full **its own** unit.

If the region is unreachable, units keep serving and load balancing (readiness is local); allocation answers `503 region-unreachable`.
