Skip to main content
Lesson 219 min read reading time

PD Tube

The single command that turns any local UI, hook, or webhook into an event your running agent can answer in one shell call. Block-once-and-return makes the agent loop work.

Watch one channel carry a threaded agent handoff

This clip shows the core PD Tube contract: send a message, reply to the exact id, then read both rows back from durable channel history.

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.

bash
$ pd tube ui:clicks
INFO: 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.000Z
Body:
{"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 continues
listening. 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.

bash
$ pd tube ui:clicks --reply "Deployed to staging. CI is green."
SUCCESS: tube: posted id=43 to ui:clicks
tube 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:

bash
# Explicit parent + stdin body.
$ printf 'roger that' | pd tube ui:clicks --reply-to=42 --sender codex
SUCCESS: 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.

bash
$ 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 --tail
INFO: 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:

html
<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.

Animated terminal recording of pd tube sending a message, replying, and reading both records back from channel history
Recorded with asciinema and rendered with agg from real Port Daddy channel history.

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.

bash
$ 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 --once
INFO: 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.