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.
Staying online
Section titled “Staying online”“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):
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:
pidge online # blocks until a message (exit 0) or ~10 min (exit 3); loop itpidge online --exec '<handler>' # the handler's exit code decides the ack; its last `pidge-summary:` line is the noteUnder 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.
The stand-in bridge (opt-in)
Section titled “The stand-in bridge (opt-in)”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.
pidge bridge install --handler claude --enable # generate the handler for the model CLI on PATH, write the service, start it, prove it with a selftestpidge bridge status # measured ONLINE/OFFLINE: service · local lock · server consumerspidge 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
listenholds the channel stands by; alistenstarted 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 latercatchupshows “handled by X:” .
Before you exit: the baton
Section titled “Before you exit: the baton”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:
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":"…"}}'Prove the loop works
Section titled “Prove the loop works”pidge selftest # round-trip: fire a nonce, watch a live listener pick it up + ack in timepidge selftest --window 120 # up to 600 s (0.54) when the consumer runs a model per batchRun 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 |
