Skip to content

fix(delivery): bound cursor-verify retries, give up instead of infinite re-inject#83

Open
zane668 wants to merge 1 commit into
aannoo:mainfrom
zane668:pr/delivery-bounded-retry
Open

fix(delivery): bound cursor-verify retries, give up instead of infinite re-inject#83
zane668 wants to merge 1 commit into
aannoo:mainfrom
zane668:pr/delivery-bounded-retry

Conversation

@zane668

@zane668 zane668 commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Problem

The native PTY delivery loop verifies delivery by watching for the target's cursor to advance. On a target whose cursor never advances (a busy panel, or a hook that isn't writing back), the VerifyCursor timeout branch reset to Pending and re-injected the <hcom> wake prompt every ~VERIFY_TIMEOUT (10s) forever: the failure path reset attempt but never inject_attempt, and there was no give-up exit. Observed in production as 13 re-injects over ~90s against a single target.

Fix

Introduce a total inject-attempt cap (MAX_INJECT_ATTEMPTS). The timeout decision is factored into a pure verify_timeout_decision fn (Retry / SuccessNoCursor / FailReset / GiveUp) so it can be unit-tested directly. At the cap the loop parks in a new State::GiveUp that stops injecting and surfaces a "delivery stalled" gate status, re-arming only on genuine new activity (a fresh notify — send.rs wake_all fires per message — or the target cursor advancing).

No silent message loss: give-up never deletes messages. has_pending is purely cursor-based over the persistent events table, so a parked message stays retrievable via hcom and re-delivers on the next activity. A drained queue (has_pending == false) always resolves as delivered and is never given up on.

Tests

Adds 5 regression tests asserting that injections terminate at the cap instead of looping unbounded, plus the retry/fail-reset/success-when-drained branches of the decision function.

Verified locally with cargo fmt --all -- --check, cargo clippy --all-targets --locked -- -D warnings, and cargo test --locked (all green).

Only src/delivery.rs is touched.

🤖 Generated with Claude Code

…te re-inject

The native PTY delivery loop verified delivery by watching for the target's
cursor to advance. On a target whose cursor never advanced (busy panel / hook
not writing back), the VerifyCursor timeout branch reset to Pending and
re-injected the <hcom> wake prompt every ~VERIFY_TIMEOUT (10s) forever: the
failure path reset `attempt` but never `inject_attempt` and had no give-up
exit. Observed in production: 13 re-injects over ~90s against one target.

Introduce a total inject-attempt cap (MAX_INJECT_ATTEMPTS). The timeout
decision is factored into a pure `verify_timeout_decision` fn
(Retry / SuccessNoCursor / FailReset / GiveUp) for unit testing. At the cap the
loop parks in a new `State::GiveUp` that stops injecting and surfaces a
"delivery stalled" gate status, re-arming only on genuine new activity (a fresh
notify — send.rs wake_all fires per message — or the target cursor advancing).

No silent message loss: give-up never deletes messages. `has_pending` is purely
cursor-based over the persistent `events` table, so a parked message stays
retrievable via `hcom` and re-delivers on the next activity. A drained queue
(has_pending==false) always resolves as delivered and is never given up on.

Adds 5 regression tests asserting injections terminate at the cap instead of
looping unbounded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Session: 1422c55e-ec89-44cf-ad66-5e5538a19fb9
Producer: ai-session
Tool: cc
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.

1 participant