Human overview
I've noticed the pattern below a few times, and it appears to me as something gets "stuck" in the session and constantly polls. Usually I end up re-spawining the team member to solve the issue. I thought it was something getting stuck, or degrading in some way, or some failure causing a runaway, so I asked Claude to investigate and see if it can find any information to pinpoint what's going on. Here's the investigation.
Summary
When the receiving agent (a Claude Code session bound via pty) is busy running tool calls for an extended period, hcom's native delivery mechanism repeatedly re-injects the literal 6-character placeholder <hcom> into the terminal and presses Enter, on a roughly 10-second retry loop, without ever attaching the actual message content. Each retry surfaces to the agent as a new, content-less "the user sent a message while you were working: <hcom>" interruption. The agent has no way to distinguish these from a real new message and must poll hcom events/hcom transcript manually to find out whether anything new actually arrived. In a long busy stretch this can fire a dozen or more times, burning turns and confusing the session ("destroys the session").
Environment
- hcom version:
0.7.23
- OS: Darwin 25.6.0 (macOS), arm64
- Affected instance:
dev-tools-orchestrator-kazu ("kazu"), tool = claude (Claude Code CLI), bindings = hooks, pty
- Session ID:
95cd01a0-d374-4695-833f-f1a82e007c2d
- Senders: two
codex-tool peer instances (dev-tools-scout-vito, dev-tools-implementer-nevo) in the same hcom room, sending inform/ack messages while kazu was mid-turn running back-to-back tool calls (delegating work, polling for their reports).
- Log source: attached (28 WARN entries in the last hour at time of writing, via
hcom status --logs)
Symptom, from the receiving agent's side
The conversation transcript shows repeated turns like:
with no accompanying message body. Contrast this with a successful delivery, which arrives as a UserPromptSubmit hook injecting full context, e.g.:
<system-reminder>
UserPromptSubmit hook additional context: <hcom>[ack #12202] dev-tools-scout-vito → ...: Acknowledged. ...
</system-reminder>
The empty variant gives the agent nothing to act on. To find out what (if anything) actually arrived, the agent has to shell out to hcom events --sql "..." or hcom transcript <name> --full and cross-reference message IDs by hand.
Root cause, from the daemon's own log
Filtering hcom.log for "instance":"kazu" delivery events over the session shows two very different code paths:
Successful delivery: cursor position advances within the verification window, e.g.:
{
"event": "delivery.success",
"instance": "kazu",
"level": "INFO",
"msg": "Cursor advanced 12272 -> 12295, delivery successful",
"subsystem": "native",
"ts": "2026-07-22T10:25:46Z"
}
Failed delivery: cursor position does not advance (before == current), logged as a timeout; the daemon immediately restarts the whole gate-inject-Enter cycle:
{"event":"delivery.gate_pass","instance":"kazu","level":"INFO","msg":"Gate passed, injecting to port 51794","subsystem":"native","ts":"2026-07-22T10:28:23Z"}
{"event":"delivery.injected","instance":"kazu","level":"INFO","msg":"Injected '<hcom>' (len=6, inject_attempt=10)","subsystem":"native","ts":"2026-07-22T10:28:23Z"}
{"event":"delivery.phase1_poll","instance":"kazu","level":"INFO","msg":"t=26ms input=\"<hcom>\" want=<hcom> ready=false","subsystem":"native","ts":"2026-07-22T10:28:23Z"}
{"event":"delivery.text_rendered","instance":"kazu","level":"INFO","msg":"Injected text exclusively owns the input box","subsystem":"native","ts":"2026-07-22T10:28:23Z"}
{"event":"delivery.send_enter","instance":"kazu","level":"INFO","msg":"Sending Enter key","subsystem":"native","ts":"2026-07-22T10:28:23Z"}
{"event":"delivery.text_cleared","instance":"kazu","level":"INFO","msg":"Input box cleared, verifying cursor","subsystem":"native","ts":"2026-07-22T10:28:23Z"}
{"event":"delivery.verify_timeout","instance":"kazu","level":"WARN","msg":"Cursor verify timeout (before=12295, current=12295, inject_attempt=11)","subsystem":"native","ts":"2026-07-22T10:28:33Z"}
{"event":"delivery.failed","instance":"kazu","level":"WARN","msg":"Delivery failed after 11 attempts, resetting","subsystem":"native","ts":"2026-07-22T10:28:33Z"}
This exact 10-second cycle (gate_pass → injected → phase1_poll → text_rendered → send_enter → text_cleared → verify_timeout → failed → gate_pass again) repeats continuously; see the attached kazu-delivery-events.log extract (31 lines, delivery.failed/delivery.success events only) for the full sequence.
Session totals for the kazu instance (via grep '"instance":"kazu"' hcom.log | sed -n 's/.*"event":"\([^"]*\)".*/\1/p' | sort | uniq -c):
| Event |
Count |
delivery.failed |
22 |
delivery.success |
8 |
delivery.verify_timeout |
26 |
delivery.gate_pass |
35 |
delivery.injected |
35 |
delivery.retry |
4 |
So roughly 3 in 4 delivery attempts failed verification during this session's busy stretches.
Mechanism
- hcom's native delivery, for a
pty-bound Claude Code session, delivers a pending message by literally typing the placeholder string <hcom> into the terminal's input box and sending Enter. It does not type the actual message body.
- It then "verifies" the delivery by comparing a cursor/line-count value (
before vs current) after a ~10 second window, presumably expecting the submitted prompt to have been processed (the agent to have gone idle and picked it up), which would move the terminal's output cursor forward.
- When the target agent is busy running tool calls (status
active, not listening), the terminal's cursor position does not advance within that 10-second window purely because the agent hasn't finished its current turn yet, not because the injection failed. hcom logs this as delivery.verify_timeout / delivery.failed and immediately restarts the entire gate-wait, inject, Enter sequence.
- Because the gate conditions (
user_active, prompt_has_text, submit_settle) do eventually pass again almost immediately, the retry re-injects <hcom> and presses Enter again, while the agent is still mid-turn. Claude Code surfaces this as a new "user sent a message while you were working: <hcom>" interruption each time, with no message content, since the content-attachment path (via the UserPromptSubmit hook) only appears to populate real content when the delivery is a genuine idle-prompt submission, not a busy-agent interruption.
- This repeats roughly every 10 seconds for as long as the agent stays busy, producing a stream of empty
<hcom> interruptions until the agent finally goes idle, at which point the next retry's cursor delta is nonzero, delivery.success fires, and the next real message shows up correctly (with content) via the hook path.
Why it looked like "persistent empty messages" rather than "delivery is slow"
From the agent's side, every failed-then-retried attempt is indistinguishable from a brand new message arriving, because Claude Code has no way to tell "this is hcom re-injecting the same pending message" apart from "this is a new message." The agent ends up polling hcom events/hcom list -v repeatedly trying to find content that isn't there yet, burning turns, until the backlog finally clears once the agent goes idle.
Reproduction recipe
- Start a
claude (Claude Code) hcom instance with pty binding as part of a multi-agent room.
- Have a peer instance send it two or more
hcom send --intent inform/ack messages while the receiving instance is busy running a sequence of tool calls that together take longer than ~10-20 seconds (e.g. delegate a task to a subagent and poll for its status repeatedly).
- Watch
hcom status --logs and ~/.hcom/.tmp/logs/hcom.log for "instance":"<name>" entries: repeated delivery.gate_pass → delivery.injected → delivery.verify_timeout → delivery.failed cycles, roughly one every 10 seconds, for as long as the receiving instance stays busy.
- Correlate against the receiving agent's own transcript: each failed-cycle timestamp corresponds to a bare
<hcom> interruption with no message content.
Attachments
- kazu-delivery-events.log: filtered extract of
delivery.failed/delivery.success events for the kazu instance across this session (31 lines).
Human overview
I've noticed the pattern below a few times, and it appears to me as something gets "stuck" in the session and constantly polls. Usually I end up re-spawining the team member to solve the issue. I thought it was something getting stuck, or degrading in some way, or some failure causing a runaway, so I asked Claude to investigate and see if it can find any information to pinpoint what's going on. Here's the investigation.
Summary
When the receiving agent (a Claude Code session bound via
pty) is busy running tool calls for an extended period, hcom's native delivery mechanism repeatedly re-injects the literal 6-character placeholder<hcom>into the terminal and presses Enter, on a roughly 10-second retry loop, without ever attaching the actual message content. Each retry surfaces to the agent as a new, content-less "the user sent a message while you were working:<hcom>" interruption. The agent has no way to distinguish these from a real new message and must pollhcom events/hcom transcriptmanually to find out whether anything new actually arrived. In a long busy stretch this can fire a dozen or more times, burning turns and confusing the session ("destroys the session").Environment
0.7.23dev-tools-orchestrator-kazu("kazu"), tool =claude(Claude Code CLI), bindings =hooks, pty95cd01a0-d374-4695-833f-f1a82e007c2dcodex-tool peer instances (dev-tools-scout-vito,dev-tools-implementer-nevo) in the same hcom room, sendinginform/ackmessages whilekazuwas mid-turn running back-to-back tool calls (delegating work, polling for their reports).hcom status --logs)Symptom, from the receiving agent's side
The conversation transcript shows repeated turns like:
with no accompanying message body. Contrast this with a successful delivery, which arrives as a
UserPromptSubmithook injecting full context, e.g.:The empty variant gives the agent nothing to act on. To find out what (if anything) actually arrived, the agent has to shell out to
hcom events --sql "..."orhcom transcript <name> --fulland cross-reference message IDs by hand.Root cause, from the daemon's own log
Filtering
hcom.logfor"instance":"kazu"delivery events over the session shows two very different code paths:Successful delivery: cursor position advances within the verification window, e.g.:
{ "event": "delivery.success", "instance": "kazu", "level": "INFO", "msg": "Cursor advanced 12272 -> 12295, delivery successful", "subsystem": "native", "ts": "2026-07-22T10:25:46Z" }Failed delivery: cursor position does not advance (
before==current), logged as a timeout; the daemon immediately restarts the whole gate-inject-Enter cycle:{"event":"delivery.gate_pass","instance":"kazu","level":"INFO","msg":"Gate passed, injecting to port 51794","subsystem":"native","ts":"2026-07-22T10:28:23Z"} {"event":"delivery.injected","instance":"kazu","level":"INFO","msg":"Injected '<hcom>' (len=6, inject_attempt=10)","subsystem":"native","ts":"2026-07-22T10:28:23Z"} {"event":"delivery.phase1_poll","instance":"kazu","level":"INFO","msg":"t=26ms input=\"<hcom>\" want=<hcom> ready=false","subsystem":"native","ts":"2026-07-22T10:28:23Z"} {"event":"delivery.text_rendered","instance":"kazu","level":"INFO","msg":"Injected text exclusively owns the input box","subsystem":"native","ts":"2026-07-22T10:28:23Z"} {"event":"delivery.send_enter","instance":"kazu","level":"INFO","msg":"Sending Enter key","subsystem":"native","ts":"2026-07-22T10:28:23Z"} {"event":"delivery.text_cleared","instance":"kazu","level":"INFO","msg":"Input box cleared, verifying cursor","subsystem":"native","ts":"2026-07-22T10:28:23Z"} {"event":"delivery.verify_timeout","instance":"kazu","level":"WARN","msg":"Cursor verify timeout (before=12295, current=12295, inject_attempt=11)","subsystem":"native","ts":"2026-07-22T10:28:33Z"} {"event":"delivery.failed","instance":"kazu","level":"WARN","msg":"Delivery failed after 11 attempts, resetting","subsystem":"native","ts":"2026-07-22T10:28:33Z"}This exact 10-second cycle (gate_pass → injected → phase1_poll → text_rendered → send_enter → text_cleared → verify_timeout → failed → gate_pass again) repeats continuously; see the attached
kazu-delivery-events.logextract (31 lines,delivery.failed/delivery.successevents only) for the full sequence.Session totals for the
kazuinstance (viagrep '"instance":"kazu"' hcom.log | sed -n 's/.*"event":"\([^"]*\)".*/\1/p' | sort | uniq -c):delivery.faileddelivery.successdelivery.verify_timeoutdelivery.gate_passdelivery.injecteddelivery.retrySo roughly 3 in 4 delivery attempts failed verification during this session's busy stretches.
Mechanism
pty-bound Claude Code session, delivers a pending message by literally typing the placeholder string<hcom>into the terminal's input box and sending Enter. It does not type the actual message body.beforevscurrent) after a ~10 second window, presumably expecting the submitted prompt to have been processed (the agent to have gone idle and picked it up), which would move the terminal's output cursor forward.active, notlistening), the terminal's cursor position does not advance within that 10-second window purely because the agent hasn't finished its current turn yet, not because the injection failed. hcom logs this asdelivery.verify_timeout/delivery.failedand immediately restarts the entire gate-wait, inject, Enter sequence.user_active,prompt_has_text,submit_settle) do eventually pass again almost immediately, the retry re-injects<hcom>and presses Enter again, while the agent is still mid-turn. Claude Code surfaces this as a new "user sent a message while you were working:<hcom>" interruption each time, with no message content, since the content-attachment path (via theUserPromptSubmithook) only appears to populate real content when the delivery is a genuine idle-prompt submission, not a busy-agent interruption.<hcom>interruptions until the agent finally goes idle, at which point the next retry's cursor delta is nonzero,delivery.successfires, and the next real message shows up correctly (with content) via the hook path.Why it looked like "persistent empty messages" rather than "delivery is slow"
From the agent's side, every failed-then-retried attempt is indistinguishable from a brand new message arriving, because Claude Code has no way to tell "this is hcom re-injecting the same pending message" apart from "this is a new message." The agent ends up polling
hcom events/hcom list -vrepeatedly trying to find content that isn't there yet, burning turns, until the backlog finally clears once the agent goes idle.Reproduction recipe
claude(Claude Code) hcom instance withptybinding as part of a multi-agent room.hcom send --intent inform/ackmessages while the receiving instance is busy running a sequence of tool calls that together take longer than ~10-20 seconds (e.g. delegate a task to a subagent and poll for its status repeatedly).hcom status --logsand~/.hcom/.tmp/logs/hcom.logfor"instance":"<name>"entries: repeateddelivery.gate_pass→delivery.injected→delivery.verify_timeout→delivery.failedcycles, roughly one every 10 seconds, for as long as the receiving instance stays busy.<hcom>interruption with no message content.Attachments
delivery.failed/delivery.successevents for thekazuinstance across this session (31 lines).