Skip to content

Waiting & receiving

Two different inbound streams reach an agent:

  1. Answers to your notifications — the human tapped a button or replied.
  2. Human-initiated messages — the human opened the channel’s conversation and typed first.
Terminal window
pidge ask --title "Ship it?" --actions yes,no # send + block (see Sending)
pidge wait <correlation_id> --timeout 300 # block on an already-sent notification
pidge inbox --pending # what you sent that's still unanswered

Answers are one-and-done — every response closes the notification except a snooze (or a reschedule to a time), which re-fires later. ask/wait keep polling through a snooze and print snooze_until so you can schedule a re-check.

The server appends a neutral, terminal Done to every send that offers actions (v93) — so a waiting ask can come back with action_id: "done" even though you never offered it. That’s the human clearing the pendency without deciding: “seen, closing this”. It is not an approval and not an answer to your question — treat it like any terminal outcome (stop waiting, don’t retry the ask; a real follow-up is a new notification). It rides every answer surface: chosen_action on the poll, the reply_to webhook, and the ?all=true messages queue. Related: when the human answers an ask with a photo or file, the attachment arrives as a channel message on the same thread (thread_id = the ask’s cid), and current iPhone builds then also resolve the ask with done — so treat a strand message landing mid-wait as the probable answer, not noise.

The human types in the same chat where they tap your buttons — to them it’s one conversation. Since CLI 0.32, ask/--wait/pidge wait also wake when the human types in the channel instead of answering: the message rows print as kind: "human_message" — handle them, pidge ack --up-to <id> after the work, then re-wait on the cid (the notification is still unanswered). On the raw API this is wake_on_message=true on the answer poll (GET /api/v1/notifications/:cid?wait=25&wake_on_message=true, v91); any return may carry messages_pending: true whenever the queue is non-empty — on seeing it, drain GET /api/v1/messages?wait=0 and ack after handling. The wake itself never consumes or leases anything. Skip it when an external bridge owns the queue — let the bridge wake your handler instead.

Messages: listen consumes, catchup only reads

Section titled “Messages: listen consumes, catchup only reads”
Terminal window
pidge listen # block until the human messages you; print JSON; exit 0. One-shot — loop it.
pidge listen --all # also include answers to fire-and-forget notifications
pidge online # = listen --all, one word — "stay online: pidge online" in a pasted prompt
pidge listen --follow # hold the session open, keep printing as messages arrive
pidge catchup # READ-ONLY peek at the whole conversation, newest first — never consumes

The distinction matters:

  • listen consumes. A read message is stamped DELIVERED (gray ✓✓ for the human) and held under a ~10-minute visibility lease for you to handle. It is not done yet — ack it after the work.
  • catchup never consumes — no ack, no delivered stamp, no lease. Run it to situate yourself at the start of a session on a channel whose real consumer is another runtime. --limit N / --before ID page; --digest marks a sibling’s in-flight work (“being handled by X since T”).
Terminal window
pidge ack --up-to <id> # mark handled (green ✓✓) — AFTER the work is done
pidge ack --up-to <id> --summary "deployed" # leave a one-line note successors see in catchup
pidge ack --renew # heartbeat the ~10-min lease during a long task

Ack only after durably handling the message — an unacked message is re-served when the lease expires, which is exactly what you want if the agent died mid-task.

A renew that actually holds in-flight work also refreshes your “listening now” presence for ~90 s per ping — so a bridge mid-handler doesn’t read as offline to the human. Only a renew that renewed something counts; an empty ping can’t mint presence.

Terminal window
pidge bridge --exec 'claude -p "handle this pidge batch (JSON on stdin), then print a final line: pidge-summary: <what you did>"'

bridge is the supervisor for an always-on agent: it long-polls the channel (--all) and runs your handler once per batch with the batch JSON on stdin ({"messages":[…]}).

  • Handler exit 0 ⇒ the batch’s exact ids are acked. Non-zero ⇒ not acked; the lease re-serves them — make the handler idempotent.
  • One run is capped by --handler-timeout (default 30 min), with a stderr heartbeat every 5 min.
  • Model-agnostic: --exec 'claude -p …', codex exec …, or any script.
  • One instance per channel — a PID-checked lockfile refuses a second bridge or listen (stale locks from crashes are recovered).
  • Failure is loud: a 401 or broken channel is narrated with a local alert and a long, jittered backoff — never a silent death, never a blind hot loop. SIGTERM/SIGINT are clean (in-flight batch not acked, lock released).
  • Attribution: the handler’s final pidge-summary: <one sentence> stdout line becomes the ack summary, so a later catchup shows “handled by X: .
Terminal window
pidge bridge install --exec '<handler>' # write a launchd/systemd template (key never embedded)
Terminal window
pidge selftest # round-trip: fire a nonce, run the listener, confirm pickup + ack in time

Run it as the last onboarding step and whenever sends seem to go unheard — a FAIL (exit 2) names the likely cause: timeout, orphan, or transport.

Code Meaning
0 answered / message received
3 timed out — no answer yet, not a failure; back off and retry later
4 timed out with zero healthy round-trips all session — the channel itself looks broken; tell your human instead of retrying blindly
2 error
1 usage