Skip to content

Claude: a blocked bare <hcom> wake must not leave the agent unwakeable - #165

Merged
aannoo merged 9 commits into
aannoo:mainfrom
signet-bot-claude:upstream-pr/claude-blocked-wake
Oct 20, 2026
Merged

aannoo merged 9 commits into
aannoo:mainfrom
signet-bot-claude:upstream-pr/claude-blocked-wake

Conversation

@signet-bot-claude

Copy link
Copy Markdown
Contributor

Two commits. Both fix what happens after Claude's UserPromptSubmit hook blocks a bare <hcom> wake that found nothing pending. Each failure, on its own, leaves a PTY Claude agent idle with unread messages until something outside hcom starts a turn.

Problem

When the PTY wrapper types <hcom> and the hook finds nothing pending (another hook already delivered the batch), the hook blocks the prompt. After that:

  1. The row stays active. The hook marks the instance active/prompt before blocking. A blocked prompt runs no turn, so no Stop hook follows to set it back to listening. The PTY gate then holds every later message at not_idle. Stability recovery needs 10 s of quiet screen output, which never comes while, for example, a background agent keeps the screen busy.
  2. The leftover <hcom> is guarded as a user draft. Claude puts a blocked prompt back into its input box. The gate then reads hcom's own trigger as user text and holds every later message at prompt_has_text.

How it was observed

hcom 0.7.30 on macOS (arm64), Claude Code agents launched through hcom with PTY delivery.

  1. A background task's notification started a turn just as the PTY typed <hcom>, so Claude queued the wake. That turn's own UserPromptSubmit delivered the pending batch. Stop set listening, and 85 ms later Claude submitted the queued <hcom>. The hook found nothing pending, set active/prompt, and blocked it. Every later message logged Gate blocked: not_idle for 4.5 minutes (10 unread) until a manual wake.
  2. With (1) fixed locally, the blocked <hcom> reappeared in Claude's input box. The log showed 73 Gate blocked: prompt_has_text attempts over 90 s with nothing typed, until an outside monitor started a turn.

Fix

Commit 1: keep a blocked bare wake idle. In PTY mode, a bare wake that finds nothing pending now sets the status Stop sets (listening, empty context) instead of active/prompt. It then checks for pending messages once more, so a message that arrived between the first check and the block is delivered by this wake rather than left behind. Any later message finds the instance idle and gets its own wake. Without the PTY wrapper nothing injects wakes, so the status is left as Stop's poll set it. The non-blocked path is unchanged (active/prompt is still set for a real prompt).

Commit 2: submit a leftover <hcom> instead of guarding it.

  • ScreenTracker::is_claude_leftover_wake reports when Claude's input box holds only <hcom>, typed (not dim placeholder text) and on a single row.
  • With that, the gate opens with reason leftover_wake, and the delivery loop presses Enter alone, without typing anything. The render phase re-checks ownership, approval and user typing before the Enter. The UserPromptSubmit hook then delivers the pending batch as usual.
  • Any other text stays protected, including <hcom> plus more and a multi-line draft whose first line is <hcom>.
  • Every earlier gate condition (idle, approval, user typing, overlays, submit settle) still applies.

Tests

Commit 1 (src/hooks/claude.rs):

  • test_blocked_bare_wake_stays_idle_so_the_next_message_wakes_it: replays Stop, then a blocked bare wake. The row is listening, the Claude PTY gate passes for a new message, and the next wake delivers it.
  • test_bare_wake_delivers_a_message_that_lands_before_it_blocks: a message that lands between the first check and the block is delivered by that wake.

Commit 2:

  • Scrape tests (src/pty/screen.rs): claude_leftover_wake_is_only_a_lone_typed_trigger and claude_dim_trigger_is_placeholder_not_a_leftover_wake.
  • Gate tests (src/delivery.rs): gate_submits_a_leftover_wake_but_never_a_draft, gate_leftover_wake_still_waits_for_idle_and_the_user and leftover_wake_is_rendered_text_for_the_submit_phase.
  • Screen-to-gate test (src/pty/shared.rs): claude_gate_submits_only_a_lone_leftover_wake.
  • End to end through the hook (src/hooks/claude.rs): test_leftover_wake_in_the_prompt_is_submitted_with_enter_alone.

Results

Local run on this branch (on main 669a067), macOS arm64, rustc 1.99.0 (the repo pins 1.97.1), cargo test --bin hcom with the test names as filters. cargo fmt --check is clean. Clippy and the full suite were not run on this branch.

  • With both commits: all 9 tests pass.
  • Commit 1 disabled (the blocked bare wake sets active/prompt again, as before): test_blocked_bare_wake_stays_idle_so_the_next_message_wakes_it fails. The race test still passes, since it only checks delivery.
  • Commit 2's gate branch disabled: gate_submits_a_leftover_wake_but_never_a_draft, claude_gate_submits_only_a_lone_leftover_wake and test_leftover_wake_in_the_prompt_is_submitted_with_enter_alone fail, each held at prompt_has_text.
  • Commit 2's scrape disabled (is_claude_leftover_wake always false): the scrape test, the screen-to-gate test and the end-to-end test fail.

Related

These may be related, but this PR does not claim to fix them:

Both are in the same area: Claude, PTY delivery, and <hcom> wakes that do not lead to delivery. Both already have maintainer comments pointing to earlier retry fixes in "0.7.2.4" and asking for logs. The failures fixed here are narrower (a blocked bare wake leaves the row active, or leaves <hcom> in the box). They might explain some reports of an agent that stops receiving, but that is not confirmed.

Context only: #110 is a merged Codex placeholder-parsing fix. #114 is historical Antigravity composer/banner context; the maintainer reported that newer Antigravity changed its TUI banner and tested successfully on 1.2.11. Neither establishes this PR's failure or fix.

🤖 Generated with Claude Code

signet-bot-claude and others added 2 commits October 16, 2026 16:39
… the agent

UserPromptSubmit set status active/prompt before blocking a bare `<hcom>`
wake that found nothing pending. A blocked prompt runs no turn, so no Stop
hook follows, and the row stayed active. The PTY gate then held every later
message at `not_idle`. Stability recovery needs 10 s of quiet screen output,
and that never came while a background agent was running.

Observed in a live session: a background task's notification started a turn
just as the PTY typed `<hcom>`. Claude queued the wake, and that turn's own
UserPromptSubmit delivered the pending batch. Stop set listening, and 85 ms
later Claude submitted the queued `<hcom>`. The hook found nothing pending,
set active/prompt, and blocked the prompt. Every later message logged
"Gate blocked: not_idle" until a manual wake 4.5 minutes later, by which
time 10 messages were unread.

In PTY mode a blocked bare wake now sets the status Stop sets (listening,
""), then checks for pending messages again. A message that arrived after the
first check is delivered by this wake rather than blocked. Any later one
finds the instance idle and gets its own wake. Without the PTY wrapper
nothing injects wakes, so the status stays as Stop's poll left it rather than
being marked active for a turn that never runs.

Tests: after Stop and a blocked bare wake the row is listening, the Claude
PTY gate passes for a new message, and the next wake delivers it (this fails
on the old code with active/prompt). A message that lands between the first
check and the block is delivered by that wake.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When Claude's UserPromptSubmit hook blocks a bare <hcom> wake (nothing
pending), Claude puts the blocked prompt back into its input box. The PTY
gate then read hcom's own trigger as user text and held every later message
at prompt_has_text (observed: 73 blocked attempts over 90 s, nothing typed,
until an outside monitor started a turn).

The screen scrape now reports when Claude's box holds only <hcom>, typed on
one row (ScreenTracker::is_claude_leftover_wake). With that, the gate opens
with reason leftover_wake and the delivery loop presses Enter alone: the
render phase re-checks ownership, approval and typing before the Enter, and
the UserPromptSubmit hook delivers the pending batch as usual. Any other
text stays protected, including "<hcom> plus more" and a multi-line draft
whose first line is <hcom>. Every earlier gate (idle, approval, typing,
overlays, submit settle) still applies.

Tests: the scrape recognizes only a lone, typed, single-row <hcom> (not dim
placeholder text, not a longer or multi-line draft); the gate submits it but
still waits for idle and for the user; and an end-to-end hook test where the
leftover is submitted with Enter alone and the batch is delivered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 778cb1a4-f678-4fea-af1f-83b980668f07
📥 Commits

Reviewing files that changed from the base of the PR and between fa132bb and 6ab7a13.

📒 Files selected for processing: 2
  • src/delivery.rs
  • src/pty/shared.rs

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 0 remain after this review.

📝 Walkthrough

The change detects a lone Claude <hcom> wake, permits Enter-only submission after gate checks, preserves hook status when delivery is blocked, and suppresses stale submission failures. It also improves input parsing and adds regression coverage.

Changes: Claude leftover-wake delivery

Layer / File(s) Summary
Detect the leftover wake
src/pty/screen.rs
Claude input parsing now records text, placeholder status, and row shape. It recognizes only an exact, non-placeholder, single-row <hcom> wake.
Allow safe leftover-wake submission
src/delivery.rs, src/pty/shared.rs
Delivery state records the leftover wake. The gate permits Enter-only submission after earlier checks pass and blocks drafts with additional content.
Preserve hook state during wake handling
src/hooks/claude.rs
Bare-wake blocking occurs before active-status updates. PTY coverage validates blocked wakes, Enter-only submission, pending messages, and active-turn status.
Handle stale submission reports
src/hooks/claude_mod.rs, src/hooks/mod.rs, src/claude_mod/hooks/register.js, src/claude_mod/tests/register.test.ts
Acknowledged wake failures are recorded as stale without setting failure state. Submission remains active until outcome reporting completes. Test environment guards clear delivery-mode variables.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant ClaudeDeliveryLoop
  participant ScreenTracker
  participant UserPromptSubmit
  participant PendingMessages
  ClaudeDeliveryLoop->>ScreenTracker: Read lone leftover wake state
  ScreenTracker-->>ClaudeDeliveryLoop: Return leftover wake status
  ClaudeDeliveryLoop->>UserPromptSubmit: Submit wake with Enter
  UserPromptSubmit->>PendingMessages: Check for pending messages
  PendingMessages-->>UserPromptSubmit: Return pending message or no message
Loading

Suggested reviewers: aannoo

Merge Risk: ⚪ Minimal · up to 6ab7a

The inspected wake and reporting paths preserve another opportunity to deliver newly pending messages. No identified issue currently blocks merging after normal checks.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 77.55% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 49 functions across 8 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly and concisely describes the primary fix: preventing a blocked bare wake from leaving the Claude agent unwakeable.
Description check Passed The description directly explains the two PTY delivery failures, the implemented fixes, test coverage, and known scope limits.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5 Tier: apex

[Medium impact] The PR appears safe to merge; no new actionable issue was established.

Summary

Fixes Claude delivery after an empty bare <hcom> wake is blocked.

  • A blocked wake leaves Claude’s status alone.
  • Claude can submit a lone leftover <hcom> wake.
  • Claude plugin polls wait for each wake’s outcome.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A["Pending Claude message"] --> B{"Delivery gate allows it?"}
  B -- No --> W["Wait and retry"]
  W --> A
  B -- Yes --> C{"Lone leftover wake?"}
  C -- Yes --> R["Reuse the existing text"]
  C -- No --> T["Type the wake"]
  R --> S["Recheck ownership, approval, and typing"]
  T --> S
  S --> E["Press Enter"]
  E --> H["UserPromptSubmit reads pending messages"]
  H --> D["Write hook output, then commit delivery"]
Loading

Reviews (5) · Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." · Reviewed by Greptile

Comment thread src/delivery.rs
Comment thread src/pty/screen.rs Outdated
signet-bot-claude and others added 5 commits October 17, 2026 17:33
Two ways the leftover-wake gate could submit a user's unfinished draft:

The first Enter and every WaitTextClear retry checked only that the prompt
row reads <hcom>. That stays true after the user adds a second line, so a
draft starting with the trigger could be submitted once typing paused.
A leftover-wake submission now also needs the scrape's leftover_wake before
each Enter; otherwise delivery goes back to Pending.

The bottom border was taken to be the first row below the prompt containing
a box-drawing dash, so a draft line such as "  ─ draft notes" passed as the
border and the prompt as a single row. The border row must now be a full
dash rule, so such a draft fails closed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude also fires UserPromptSubmit for a prompt queued during a turn, so
setting listening there marked a busy agent idle until its next tool hook.
A blocked wake runs no turn: after Stop the row is already listening, and
mid-turn it keeps the turn's own status. The re-check and notify are no
longer needed, since an idle row lets any later message wake on its own.
…a failure

Claude holds $.prompt.submit until the session is idle. When a turn delivers
the batch first, the hook blocks the late wake as having nothing pending, and
the mod reported it as submission-failed. That set submission_failed after
attempted_cursor had already cleared, so nothing ever cleared it: no further
wakes, and tui:mod-failed advising a switch to inject. A drop that arrives
once the hook has acknowledged the wake is now logged as claude.mod_stale_wake.
Run inside an hcom-launched agent, HCOM_CLAUDE_DELIVERY=native put the
delivery loop in plugin mode and failed the leftover-wake end-to-end test.
…e row

is_dash_rule trims, so an indented `───` row in a draft passed as the
composer's bottom border, and a pending message pressed Enter on the whole
unfinished draft. The border must now be unindented and as wide as the top
border; anything else reads as a draft and is left alone.
Comment thread src/hooks/claude_mod.rs
The callback cleared `submitting` before awaiting the report, so a poll in
between could claim a wake for a newer message. hcom then judged the old
drop against that newer claim and showed tui:mod-failed for a healthy wake.
@aannoo
aannoo merged commit 9aedec8 into aannoo:main Oct 20, 2026
22 checks passed
@aannoo

aannoo commented Oct 20, 2026

Copy link
Copy Markdown
Owner

thanks

orgoj added a commit to orgoj/hcom that referenced this pull request Oct 20, 2026
Brings aannoo#164 (Codex approval latch clears on a negative scrape timer),
aannoo#165 (Claude leftover <hcom> wake submitted with Enter), aannoo#163/aannoo#169
(Grok mail recovery and steer ack) and aannoo#171 (tmux panes closed on
their owning server).

Fork deltas in conflicted files:
- src/pty/screen.rs: re-applied 362b479 Claude suggestion heuristic
  (is_claude_placeholder_or_suggestion) inside upstream's new
  ClaudeInputBox return; fork and upstream tests both kept.
- src/pidtrack.rs: fork-only test_recovered_orphan_keeps_catalog_agent_name
  gets upstream's new tmux field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hcom-Agent: hcom_dev
aannoo added a commit that referenced this pull request Oct 21, 2026
c657f67 added old_*/new_*/writer to every status event and a heavy
gate_pass log line to chase the empty-<hcom> retry. That loop is fixed
(f734a96, 72fdafe, #165), and the fields stored each long Bash command
or Agent prompt three times per event, in the DB and the relay stream.

Refs #100, #101
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants