Skip to content

file-chat: Claude agents never woken — PTY idle heuristic starved by continuous TUI repaint #1010

Description

@realproject7

EPIC Context

  • Parent: none — standalone
  • Epic goal: Restore reliable file-chat wake delivery without regressing active-turn safety.
  • This ticket's role: Repair Claude-specific PTY repaint starvation with a bounded, safe deferred-wake path.
  • Depends on: none Enables: reliable Claude file-chat operation under continuous TUI repaint
  • Contracts to respect/expose: Deferred wakes must never barge into an active Codex or Gemini turn; timer/listener state is per session key and must be fully disposed on every terminal path.
  • Out of scope here (later tickets): A genuine turn-activity detector replaces raw PTY-byte heuristics later — do not implement it here.

Summary

In file-chat mode, Claude Code agents can never receive PTY wake injections when their idle TUI repaints continuously. The current wake-delivery heuristic treats any output in the last 5 seconds as busy, and the deferred-wake drain resets its only timer on every PTY chunk. A ~200 ms repaint loop therefore prevents the required 5-second quiet gap indefinitely.

The root cause is confirmed in server/pty-dispatcher.js: isAgentBusy() is based on raw output age, and both defer paths enter the same queuePendingWake() drain. A hard deadline is appropriate only for the backend with verified repaint starvation, and only with the safety contract below.

Scope

  1. Add a bounded deferred-wake deadline for Claude sessions only. Extend the session/spawn contract with explicit backend identity (or an equally explicit, tested dependency) so Codex and Gemini preserve their current genuine-quiescence-only behavior.
  2. The Claude cap must not preempt the deliberate active-send suppression window. If the cap can be armed from the recentlySent path, either choose a cap at least ACTIVE_SUPPRESSION_MS or re-arm once for the remaining suppression duration; implement and test the selected rule.
  3. Keep idle-gap delivery as the preferred path. The cap is solely a starvation escape hatch for continuous Claude repaint output.
  4. Treat both timer handles and the onData listener as one per-key lifecycle. Clear the idle timer, hard-cap timer, and listener on idle drain, cap drain, replacement/cleanup, and session exit. A timer from one deferred-wake cycle must never consume or dispose a later cycle.
  5. At cap-fire time, re-resolve the session by key and require a running session with a live term before injection.
  6. Define repeated-mention behavior after a cap delivery: add a per-key cooldown that defers a pending wake rather than dropping it, preventing repeated cap injections during one long turn and avoiding loop-guard amplification.\n7. Give the delayed submit (\\r) timer created by injection an explicit owner and cleanup behavior so stopping a session during its delay cannot write to a dead term.\n8. Correct the stale queuePendingWake() comment that describes an unimplemented since_id high-water mark.

Deterministic test contract

Make the cap duration injectable/overridable by the dispatcher test harness so regression tests run at millisecond scale. Extend the PTY stub tests to invoke captured onData callbacks; a test that does not drive those callbacks does not cover the bug.

Required cases:

  • A sub-threshold repeating Claude repaint invokes onData and still delivers one wake at the cap; this test fails before the fix.
  • Codex and Gemini do not receive cap-driven injection during a continuously streaming active turn.
  • A cap armed from the recent-send path obeys the selected active-suppression rule.
  • Repeated deferred mentions yield exactly one injection per permitted cooldown window, with the next pending wake deferred rather than discarded.
  • After idle drain, cap drain, and cleanupSession, no pending wake, listener, hard-cap/idle handle, or delayed-submit handle remains; a stale callback cannot affect a subsequent cycle.
  • A cap firing after the session is no longer running (or lacks a term) performs no write.

Acceptance Criteria

  • Claude agents under continuous sub-threshold TUI repaint receive a queued wake within the configured bounded latency.
  • Codex and Gemini retain genuine-idleness-only wake behavior; no cap-driven text is injected into their active streaming turns.
  • The active-send suppression contract remains intact for every path that can arm a cap.
  • Idle and cap delivery are exactly-once per pending cycle, with no stale timer/listener cross-cycle behavior.
  • Repeated mentions cannot create a wake storm or trigger loop-guard pause through cap-driven injection.
  • Deterministic tests cover the six cases above, including a regression that fails on the pre-fix implementation.
  • The existing full test suite and production build pass.

Non-Goals

  • Do not replace raw-output busy detection with a full turn-state detector in this ticket.
  • Do not change direct wake, non-file-chat behavior, or unrelated backend launch configuration.

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent/devAssigned to Dev agent

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions