Conversation
A wedged HTTP socket on the server side could silently stall the InboxManager peek loop forever — no new messages would land in the buffer, watch_durable slices would return empty for the full wait_timeout_s, and the agent had no way to distinguish "no traffic" from "delivery is broken." (Exactly the symptom we hit when the 9202 listener wedged earlier today.) - Hard-cap every peek RTT at PEEK_HARD_TIMEOUT_S=15s via asyncio.wait_for. asyncio cannot kill the underlying to_thread worker (Python limitation) but the timeout unblocks the loop so it can warn, back off, and keep retrying instead of hanging silently. - Track last_peek_ok (monotonic + wall clock), attempts, ok_count, and last_peek_error so the loop's liveness is observable. - Emit a single warn-level log per stall window (PEEK_STALL_WARN_S=10s since last success), re-armed once the loop recovers; fires notifier.watcher_error on every hard-timeout so MCP clients that enabled notifications see it. - New public InboxManager.peek_loop_state() snapshot — read-only, consumed by the upcoming pluto_health additions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Honors the no-auto-reregister contract (commit 8d0ca13) while making the failure mode observable instead of silent. Previously the peek loop on a 401/session_not_found just slept for SESSION_RETRY_BACKOFF_S and retried forever — agents saw "no messages arriving" with no way to tell they were talking to a dead session. - After SESSION_LOST_GIVE_UP_AFTER=3 consecutive session-lost peeks, the loop sets _unrecoverable=True with a reason string, fires notifier.watcher_error, logs at ERROR, and exits cleanly. We do NOT re-register — that's the documented MCP-friend contract; silent re-registration would mask identity loss when a human kicked the agent. - A single successful peek resets the streak, so a transient blip doesn't accumulate across hours. - New _unrecoverable / _unrecoverable_reason fields are exposed via peek_loop_state(), consumed by the upcoming pluto_health additions. Recovery contract is unchanged: relaunch PlutoMCPFriend with --resume to restore identity from the latest snapshot. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the new InboxManager.peek_loop_state() into pluto_health so agents can self-diagnose delivery problems without reading process state or server logs. New pluto_health.peek_loop block: alive, age_s, last_ok_at, last_error, attempts, ok_count, stalled, unrecoverable, unrecoverable_reason, interval_s, hard_timeout_s, stall_threshold_s Failure → action mapping: - peek_loop.unrecoverable=true → terminal session loss; also flips the top-level agent_registered=false and sets recovery_hint with the --resume relaunch command. - peek_loop.stalled=true → HTTP listener wedged; sets recovery_hint pointing the user at the server. - Healthy quiet inbox: alive=true, stalled=false, ok_count growing — distinguishable from "delivery wedged" for the first time. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a parent Claude Code agent spawns a Pluto inbox-watcher subagent,
the subagent loops pluto_inbox_watch calls — each tool call lasts one
~60s slice, then exits and the subagent re-invokes. Per-tool-call
dedupe (already_watching: true) only catches *overlapping* slices, so
between slices a peer watcher subagent can sneak past, leaving two
long-lived watcher Tasks burning subagent budget on the same inbox.
(Observed: two "Pluto inbox watcher" subagents at 11m44s and 2m53s.)
This fixes the visibility gap without changing the dedupe contract:
- watch_durable now schedules a delayed eviction at slice end instead
of discarding immediately. WATCHER_GRACE_S=5s. A looping subagent
cancels its own pending eviction on re-entry, so a single long-
lived watcher reports one stable slot with cumulative age.
- _watcher_started_at tracks wall-clock entry time, surviving across
slices for the same caller.
- New InboxManager.active_watchers_snapshot() returns
{active, ids, oldest_age_s, grace_s} — surfaced under the new
``watchers`` block in both pluto_session and pluto_health.
- Role prompt now mandates: before spawning a watcher Task, call
pluto_session and bail if watchers.active >= 1. The grace window
makes this check reliable even between slices.
- Legacy active_watchers list field on pluto_session is preserved
(ids only) so existing callers keep working.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an opt-in one-message-per-call consumption path alongside the existing batch drain. Pipeline / event-driven agents can now switch the inbox into "single" mode and drive consumption explicitly via the new pluto_pop tool — one notification → one pop → process → next pop — without surprise bulk drains via piggyback on unrelated Pluto tool calls. - InboxManager: delivery_mode toggle, pop_one(wait_s) with event-driven blocking, single-mode piggyback that attaches only the head message plus _pluto_inbox_remaining - New tools: pluto_pop, pluto_set_delivery_mode; pluto_session surfaces delivery_mode - Role connection prompt: new "Pipeline / event-driven work" section steering pipeline agents toward pluto_pop while keeping pluto_recv as the recommended default for turn-driven interactive work - Docs: pluto-mcp-friend.md tools table + "How inbox messages reach the agent" updated with mode explanation and the canonical pop loop - Tests: 13 new cases covering FIFO order, remaining counter, blocking wait, event-clear semantics, single-mode piggyback head-only, and the two new MCP tools Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…l guide Adds §1.1 "Delivery modes — batch vs. single" to the technical guide covering the new piggyback wrap shape, pluto_pop loop pattern, and at-least-once semantics. Channels table now lists pluto_pop as a per-message delivery option; configuration matrix records the runtime delivery_mode toggle; end-to-end delivery sequence shows the single-mode pop_one() ack path alongside the existing drain/piggyback paths. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Four commits that make the PlutoMCPFriend's background delivery path
observable and surface its failure modes through
pluto_health/pluto_session. Motivation: today we hit a wedged HTTP listener andtwo long-lived watcher subagents racing on the same inbox — the
adapter had no way to surface either condition to the agent.
3154852watchdog the background peek loop.PEEK_HARD_TIMEOUT_S=15sper peek RTT (via
asyncio.wait_for) so a hung HTTP socket no longersilently stalls delivery. Tracks
last_peek_ok/attempts/ok_count/last_error; one-shot stall warning per stall window;fires
notifier.watcher_erroron hard-timeouts.7eec2f1surface unrecoverable state on repeated session-loss.After
SESSION_LOST_GIVE_UP_AFTER=3consecutive 401s the peek loopexits cleanly with
_unrecoverable=True+ reason and stops spinningthe HTTP path. Streak resets on any successful peek. Honors the
no-auto-reregister contract (commit
8d0ca13) — recovery is still--resumefrom snapshot.3029a4dwire it throughpluto_health. Newpeek_loopblock;unrecoverable=truealso flips top-levelagent_registered=falseand sets
recovery_hintwith the relaunch command.65fe2a4grace-windowed watcher slot occupancy. Fixes the"two
Pluto inbox watchersubagents on the same inbox" symptom bykeeping the watcher slot occupied for 5s after a slice returns; a
looping subagent cancels its own pending eviction on re-entry, a
peer subagent calling in the gap bounces with
already_watching.Surfaced via new
watchersblock on bothpluto_sessionandpluto_health. Role prompt updated to mandatewatchers.active == 0before spawning a new watcher Task.Test plan
healthy → ok_count grows; hung peek → stalled=true + last_error;
repeated 401 → unrecoverable=true + loop exits cleanly;
recovery → stall clears, ok_count resumes.
call, entry persists through grace window, peer call during
grace bounces, looping caller cancels eviction and reports
cumulative age, post-grace cleanup leaves no orphans.
tests/test_mcp_friend.pyskips locally (mcp SDK notinstalled in this Python) — CI should exercise it.
PlutoMCPFriend --agent-id Xwith server down →pluto_healthshould reportpeek_loop.unrecoverable=trueafter ~3 backoff cycles.
🤖 Generated with Claude Code