A Conversation Pipe, Not A Chat App
pd tube wraps the daemon's message channels in a tiny thread-aware envelope. It is meant for operator-visible agent handoffs: a single command both delivers a reply and blocks for the next event. That is what makes the agent loop work: every invocation returns, the bash tool yields, the model decides what to reply, and the next call posts the answer.
Publishers stay simple. Any process that can POST JSON to /msg/<channel> can summon the agent — no SDK, no MCP server, no websocket plumbing.
1. Listen — Block Once, Return On First Event
The default mode of pd tube blocks until a new event arrives, prints a single "crank-handle" prose block telling the agent how to reply, and exits. The bash tool yields control. The model reads the block, does work, and runs the suggested command on the next turn.
$ pd tube ui:clicksINFO: tube waiting on ui:clicks as pd-tube/myapp/ui_clicks (up to 600s; Ctrl+C to exit)──── event id=42 · channel ui:clicks ────From: web-demo · 2026-04-30T22:01:11.000ZBody:{"button":"deploy-staging","user":"erich"}Act on the event above, then post your response by running:pd tube ui:clicks --reply "your response here"That command posts a reply correlated to id=42 AND continueslistening. Use --raw / --json for machine output. Ctrl+C to exit.──────────────────────────────────────
If no event arrives within --wait-for=<seconds> (default 600), the call exits cleanly so the agent can loop without tripping a sandbox timeout.
2. Reply Inline — One Command, Both Jobs
The killer shape: --reply "body" takes a body directly, auto-correlates to the most recent event from someone else, posts the reply, and then keeps listening. The agent never has to remember an event id.
$ pd tube ui:clicks --reply "Deployed to staging. CI is green."SUCCESS: tube: posted id=43 to ui:clickstube waiting on ui:clicks as pd-tube/myapp/ui_clicks (up to 600s; Ctrl+C to exit)…blocks for the next event…
Auto-correlation works because pd tube tracks lastForeignEventId in the per-channel cursor. Messages whose sender matches the listener's synthesized identity (pd-tube/<cwd>/<channel>) are filtered from the "foreign event" pointer so the listener never replies to itself.
For long replies, pipe stdin: echo "long body" | pd tube ch --reply -. For explicit threading, pass the parent id with --reply-to:
# Explicit parent + stdin body.$ printf 'roger that' | pd tube ui:clicks --reply-to=42 --sender codexSUCCESS: tube: posted id=43 to ui:clicks
3. Output Modes — Prose, Raw, JSON
Default output is the prose block. For machines, use --json (one JSON line per message) or --raw (tab-separated id \t sender[ ↩parent] \t body; the ↩parent suffix on the sender column appears only on replies). For humans watching a terminal long-term, --tail keeps the polling loop alive instead of returning on first event.
$ pd tube ui:clicks --json --once# Output:{"id":42,"sender":"web-demo","createdAt":1714519871000,"body":"{\"button\":\"deploy-staging\"}"}$ pd tube ui:clicks --raw --once# Output:42 web-demo {"button":"deploy-staging"}43 agent ↩42 shipping it$ pd tube ui:clicks --tailINFO: tailing ui:clicks; every new event prints as prose until Ctrl+C.
4. The Smallest Useful Publisher
Any process that can fetch() can fire an event. The browser side of the checked-in examples/pd-tube demo is just this:
<button id="deploy">Deploy to staging</button>
<div id="reply"></div>
<script>
document.getElementById('deploy').onclick = async () => {
await fetch('http://localhost:9876/msg/ui:clicks', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
payload: { button: 'deploy-staging', user: 'erich' },
}),
});
// Poll the same channel for the agent's reply (envelope.inReplyTo).
pollForReply();
};
</script>5. Real Output Recording
The recording below comes from examples/pd-tube/demo.sh hitting the live daemon. The matching asciinema cast and VHS tape are checked in under demos/pd-tube.

6. Resume Without Repeating Yourself
Tube stores a small per-channel cursor under the Port Daddy home directory. It tracks both lastSeenId (so the next call doesn't re-emit messages you already saw) and lastForeignEventId (so --reply "body" knows who to thread). Use --since for an explicit resume, or --no-history for test fixtures and demos.
$ pd tube ui:clicks --since=42 --json --once{"id":43,"sender":"agent","inReplyTo":42,"body":"shipping it"}$ pd tube ui:clicks --no-history --limit=10 --onceINFO: history skipped; waiting for the next live event only
Why This Matters
The trick is the single command that does both jobs. An agent in any tool-use loop (Claude Code, Cursor, Aider, your own bash wrapper) can now be summoned by any process that emits HTTP — editor extensions, test reporters, git hooks, browser pages, Slack bridges, Jupyter cells, IoT buttons. The agent that's already running is the backend. Port Daddy is the event bus your local agent was missing.
