Skip to content

Spike: wake an idle Claude Code session over its own socket, and make mail delivery transport-aware #320

Description

@Wirasm

Spike: wake an idle Claude Code session over its own socket, and make the mailbox's delivery transport-aware. Prove it, record what is true, and only then decide what to build.

Why now

The Claude Code half of helm's mailbox is built on a premise that has expired.

hooks/helm-mail.mjs delivers on UserPromptSubmit and the notice then spends thirteen lines teaching the agent to arm its own watch — including that an unmatched zsh glob is fatal, that Monitor needs persistent: true and no timeout_ms, and "this is the last time you are told any of the above." All of that exists to work around one sentence in AGENTS.md:

Nothing outside a Claude Code session can [start a turn], so the notice instead tells the agent to arm its own watch; being notified is the wake.

That is no longer true. Claude Code 2.1.224+ ships cross-session messaging: per-session Unix sockets under /tmp/cc-socks/<pid>.sock, disk-based discovery, and — decisively — a message delivered to an idle session's socket arrives as a new user turn and wakes it. github.com/ray-amjad/peer-sessions is a worked example of a fleet built on exactly this, and this session is itself using the mechanism to steer subagents by name.

So the most elaborate machinery in the mailbox may be replaceable by a socket write.

Scope

In: claude → claude, claude → pi, pi → pi, pi → claude.

Out, deliberately: codex. It has no mailbox integration at all today — codex appears nowhere in hooks/, pi/extensions/ or either mail skill — so it is a claim problem before it is a wake problem. Solve it when it is wanted. Nothing in this spike should make it harder; note anything that would.

The framing that makes this small

Sending is already runtime-neutral: it is writing a JSON file into a directory, and anything that can write a file can send. What is per-runtime is only two things — claiming a mailbox so you are addressable, and waking you when you are idle.

So this is not an N×M problem. It is N claims + N wakes, and only the wake half is in question here.

runtime claim wake today wake proposed
claude ✅ hook arm-a-watch (Monitor), or none while idle socket poke
pi ✅ extension in-process sendUserMessage unchanged
codex ❌ none out of scope

The record does not change. The file mailbox stays canonical and runtime-neutral; only delivery becomes transport-aware. That keeps every rule bought with an incident: notice carries the path and never the body, operator reserved, retire-never-delete.

Assumptions to prove — each is a measurement, not a discussion

  1. A socket poke wakes a genuinely idle helm-hosted session, as a new user turn, with no watch armed. Measure in a helm pane, not only in a bare terminal.
  2. helm can resolve handle → pid → socket reliably. owner.json records the pid and Swift still joins owners on the pid, and the claim can record a pid that was never the agent's #247 fixed how it is recorded. Confirm it is the pid whose socket exists, not an ancestor or a wrapper.
  3. What happens when the socket is stale or absent — a session that exited, a pid reused, a session started before the feature. The fallback must engage silently and correctly.
  4. Deliver-before-turn still works as the fallback, so the change is strictly additive: if the socket path changes in a Claude release, worst case is exactly today's behaviour.
  5. Whether Claude Code's own loop throttling is sufficient. It ships inbound policies and loop throttling; helm adding a second cap on top may be redundant or may conflict. Measure before building one.
  6. THE ONE MOST LIKELY TO BITE — whether a socket-delivered wake is distinguishable, from inside the session, from the operator typing. See below.
  7. Whether the sender or helm should poke. helm already reads owner.json, knows the pid and owns the pane, so a sender need never learn the recipient's runtime. Confirm nothing forces the sender to know.

The wake cap, and why it is the sharp end

hooks/helm-mail.mjs says it plainly:

THERE IS NO WAKE CAP, and its absence is a consequence rather than an omission. kild's DEFAULT_WAKE_CAP = 3 existed because delivering on Stop CONTINUED a turn… Delivering on UserPromptSubmit spends nothing — the notice rides a prompt the operator just typed — so there is no runaway to cap.

The cap is unnecessary today precisely because Claude cannot be woken. This spike succeeding is what creates the need. pi carries WAKE_CAP = 3 for exactly the reason that would now apply to Claude: sendUserMessage starts a turn from nothing, so two agents replying to each other burn until the money runs out.

And pi's counter is "consecutive wakes with no operator-driven turn between them" (index.ts:703), reset when the operator speaks. A socket-delivered message arrives as a user turn. If that is indistinguishable from the operator typing, the reset fires on every wake and the cap can never trip — a cap that looks present and counts nothing. Assumption 6 exists to catch that, and it is the one worth measuring first.

Note also that AGENTS.md currently claims both runtimes cap at 3. Only pi does. That doc line is wrong today and gets more wrong if this lands.

Deliverables

  • A written record in ~/.prp/<key>/research/ naming, per assumption: what was measured, how, and what was true. A refuted assumption is a result — say so rather than quietly working around it.
  • A recommendation on where the wake cap belongs if one is needed at all: helm (once, for every runtime), the runtimes (as now, twice and inconsistently), or nowhere because the platform already throttles.
  • A recommendation on courier vs sender.
  • No production change in this ticket. If the spike says build it, that is a second ticket with this one's measurements as its argument.

Acceptance

  • Every assumption above has a verdict backed by something that was run.
  • The codex gap is described precisely enough that the later ticket does not have to rediscover it.
  • If the socket path proves unstable or undocumented in a way that makes it a poor foundation, that is a valid and useful outcome — record it and the arm-a-watch dance stays.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions