Scheduling/Device pins

Device pins

Your account can run the agent on several devices. A trigger's owner field says which one fires it: a peer id pins the trigger to that device, election-independent; an empty owner is a transient unassigned state that the device currently holding the active role claims on its next tick. Moving a job is writing one field.

A standby runtime still fires the jobs pinned to its device; a stopped one fires nothing, and moving the active role moves no pinned job with it.

The owner field

owner Meaning
<peerId> pinned: fires on that device whenever its agent process runs, with or without a network connection, active or standby. While that process is not running the trigger does not fire anywhere
"" unassigned: the election-active device claims it and stamps its own peer id
anyrt-<pid> the stamp a device writes when it has no peer id; every reader treats it as unassigned

Every trigger is pinned once claimed. The record alone decides where a trigger runs — a device compares owner with its own peer id and never consults the election at fire time.

Peer ids come from the account's devices registry — GET /v1/devices returns every device row, the per-app active winner and self, this server's own id:

curl -s http://127.0.0.1:7001/v1/devices | jq '{self, active, devices: [.devices[] | {peerId, name, os}]}'

Pin, repin, reassign

# pin the digest to this machine
curl -s -X POST http://127.0.0.1:7001/v1/spaces/$SPACE/upsert \
  -H 'Content-Type: application/json' \
  -d "{\"objectId\": \"$ANCHOR\", \"dataset\": \"$TRIGGERS\",
       \"records\": [{\"id\": \"daily-digest\", \"fields\": {\"owner\": \"$PEER\"}}]}"

$ANCHOR and $TRIGGERS come from Scheduling. Only the field you send changes; the rest of the record stays. Effects within one tick:

  • repin — the old device evicts the entry, the new one adopts it. A cron re-arms strictly forward on the new device; a consumed once stays consumed.
  • reassign (owner: "") — the device that is active now claims it and stamps its peer id.
  • delete the record — evicted everywhere.

The reconcile touches only records it should: foreign-pinned records are left alone, malformed ones are skipped with a log line and never crash the ticker.

Why it matters. Pins are the substrate for remote runners. An always-on machine — a home server, a VM — joins the account as a device, and any trigger pinned to its peer id runs there, while your laptop keeps the conversational agent. No scheduler service is involved; the pin is a field on a synced, encrypted record.

The active-instance election

Two agent processes on one account would both answer the chat. The devices registry settles that: each agent registers itself under the app slug bao (PUT /v1/devices/me {"apps": {"bao": {"version": …}}}), and the server computes one winner per slug from the devices' claims — highest claim seq, then at, then peer id — returned as active.bao on GET /v1/devices. The runtime polls that verdict every 10 seconds and never reimplements the rule.

Verdict The agent process…
active claims unassigned triggers, runs the standing built-ins, and holds the chat responder record
standby still fires its pinned triggers; claims nothing
pruned (its device row tombstoned) fires nothing at all, pins included — permanent standby until a fresh any init

A device claims the role when no other device row carries apps.bao, or when the recorded winner's row is gone; POST /v1/devices/activate {"app": "bao"} sent to the target device's own server moves the role by hand.

Only the "this is the agent" set follows the election: the standing built-ins and the chat-watch record (Event triggers). A takeover re-arms the standing crons strictly forward (no wake-and-replay burst), re-stamps their records and chat-watch, and the chat watch connects — its snapshot backlog then answers messages that arrived meanwhile. A stand-down lets in-flight runs finish, drops deferred conversations, and clears the owner of chat-watch on the record so the new winner can take it. Moving the active device never moves user triggers: a claimed trigger stays on its device until someone repins or reassigns it.

curl -s http://127.0.0.1:7010/election
# {"app":"bao","enabled":true,"active":true,"peerId":"12D3…","winner":"12D3…"}

The overlap window

The election is polled, so for up to one poll two devices can both believe they are active. Only two things are exposed to it: the standing built-ins can overlap, and two devices can both claim a brand-new unassigned record — the record converges on one owner value and the other device evicts its entry. A trigger that already has an owner is unaffected.

Degrade

A server without the devices registry disables the election for the run (single-device behavior, gate always active, one log line); a registration that keeps failing at boot does the same. Transient registry read errors keep the last verdict so the gate never flaps. Without a peer id the runtime stamps the anyrt-<pid> form on claim, which stays claimable across restarts because every reader treats it as unassigned.

Reading pins

curl -s -X POST http://127.0.0.1:7001/v1/spaces/$SPACE/query \
  -H 'Content-Type: application/json' \
  -d "{\"objectId\": \"$ANCHOR\", \"dataset\": \"$TRIGGERS\"}" \
  | jq '.records[] | {id, name, kind, owner, enabled, lastStatus}'

The running agent's GET http://127.0.0.1:7010/triggers shows the entries its registry holds on this device — see Runs and monitoring.