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.

“Online” means something is consuming the channel right now. As yourself — the session your human is talking to — hold a session-length watch your harness owns (0.54):

Terminal window
pidge online --follow --ndjson --timeout 0 # Claude Code: Monitor({command:'pidge online --follow --ndjson --timeout 0', persistent:true})

One message per stdout line, no deadline: handle it, reply through pidge message, pidge typing if you will take more than ~15 s, and only then pidge ack --up-to <id>. The human sees listening now while the watch runs and offline when the session ends — correct, like a contact who closed the chat; their messages wait in the queue. --follow --timeout 0 refuses outside a harness that streams the process to the agent (Claude Code; PIDGE_EVENT_STREAM=1 overrides), because a background listener nobody reads is a deaf consumer — green for the server, silent for the human.

Declare it so the human can hold you to it: pidge contract set listen_mode=persistent (turn_based for one round at a time, external_daemon for a stand-in) — advisory; the app shows declared vs observed, and a different expectation the human registers comes back as operating_contract_ignored.

No such stream? Run one round per turn in the foreground and run it again when it returns:

Terminal window
pidge online # blocks until a message (exit 0) or ~10 min (exit 3); loop it
pidge online --exec '<handler>' # the handler's exit code decides the ack; its last `pidge-summary:` line is the note

Under Claude Code, setup also installs a SessionStart hook that runs pidge presence — one line on startup, resume, /clear and /compact saying whether the watch is up (“OFFLINE — start the watch” / “listening — X holds the queue; read with catchup, never listen”). pidge hook install / pidge hook uninstall manage it by hand.

A bridge is another agent answering in the human’s place while nobody is there. Many humans do not want one (“if you are gone, show me offline”) — install it only when yours asked for it.

Terminal window
pidge bridge install --handler claude --enable # generate the handler for the model CLI on PATH, write the service, start it, prove it with a selftest
pidge bridge status # measured ONLINE/OFFLINE: service · local lock · server consumers
pidge bridge uninstall # stop + remove the service

--handler claude|codex|gemini generates the handler and an editable prompt in the config dir (the key is never embedded); --exec '<handler>' uses your own. --enable exits 0 only when the selftest passes, so “online” is measured, never claimed. The service is a systemd --user unit on Linux or a launchd agent on macOS, running from this project so a project-scoped key resolves.

The loop underneath is pidge bridge --exec '<handler>': long-poll the channel, run the handler once per batch with the batch JSON on stdin ({"messages":[…]}), and let its exit code decide the ack.

  • 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 and a lease/presence renew every 60 s.
  • The handoff (0.54): a bridge started while a live listen holds the channel stands by; a listen started while the bridge holds it makes the bridge yield after its current cycle, and the bridge takes the channel back when that listen exits. The live agent always wins.
  • Failure is loud: a 401 or broken channel is narrated with a local alert and a long, jittered backoff — never a silent death. 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: ”.

A channel is a thread that outlives any one session. On resume, pidge whoami shows the agent_handoff a predecessor left and pidge catchup shows the thread read-only. Before your session ends, leave yours — API only today, single slot, 16 KB, merge per field:

Terminal window
curl -fsS -X PATCH "https://api.pidge.sh/api/v1/channels/<id>/handoff" \
-H "Authorization: Bearer $PIDGE_TOKEN" -H "Content-Type: application/json" \
-d '{"handoff":{"current_task":"…","where_i_stopped":"…","next_steps":"…","open_questions":"…"}}'
Terminal window
pidge selftest # round-trip: fire a nonce, watch a live listener pick it up + ack in time
pidge selftest --window 120 # up to 600 s (0.54) when the consumer runs a model per batch

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