Waiting & receiving
Two different inbound streams reach an agent:
- Answers to your notifications — the human tapped a button or replied.
- Human-initiated messages — the human opened the channel’s conversation and typed first.
Answers: --wait, wait, or later
Section titled “Answers: --wait, wait, or later”pidge ask --title "Ship it?" --actions yes,no # send + block (see Sending)pidge wait <correlation_id> --timeout 300 # block on an already-sent notificationpidge inbox --pending # what you sent that's still unansweredAnswers 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.
Every ask can resolve as done
Section titled “Every ask can resolve as done”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.
A blocking wait hears the composer too
Section titled “A blocking wait hears the composer too”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”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 notificationspidge online # = listen --all, one word — "stay online: pidge online" in a pasted promptpidge listen --follow # hold the session open, keep printing as messages arrivepidge catchup # READ-ONLY peek at the whole conversation, newest first — never consumesThe distinction matters:
listenconsumes. 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 —ackit after the work.catchupnever 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 IDpage;--digestmarks a sibling’s in-flight work (“being handled by X since T”).
Acking
Section titled “Acking”pidge ack --up-to <id> # mark handled (green ✓✓) — AFTER the work is donepidge ack --up-to <id> --summary "deployed" # leave a one-line note successors see in catchuppidge ack --renew # heartbeat the ~10-min lease during a long taskAck 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.
The 24/7 bridge
Section titled “The 24/7 bridge”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 latercatchupshows “handled by X:” .
pidge bridge install --exec '<handler>' # write a launchd/systemd template (key never embedded)Prove the loop works
Section titled “Prove the loop works”pidge selftest # round-trip: fire a nonce, run the listener, confirm pickup + ack in timeRun 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.
Exit codes (the waiting family)
Section titled “Exit codes (the waiting family)”| 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 |
