feat(sprout-acp): parallel agent pool, heartbeat timer, and just goose recipes#64
Merged
tlongwell-block merged 8 commits intomainfrom Mar 14, 2026
Merged
Conversation
Steps 1-3 of parallel agents + heartbeat implementation: - queue.rs: HashSet<Uuid> in_flight_channels, retry_after throttle, requeue_preserve_timestamps, has_flushable_work, Clone derives - pool.rs: AgentPool, OwnedAgent, PromptResult, run_prompt_task - config.rs: --agents, --heartbeat-interval, --heartbeat-prompt flags All 116 tests pass. main.rs loop rewrite (Step 4) follows.
Step 4: Replace 2-branch select with 5-branch biased select. - N-agent startup with AgentPool - dispatch_pending: flush queued work to idle agents - handle_prompt_result: reclaim agent, requeue on failure, respawn on exit - recover_panicked_agent: JoinError::id() -> task_map O(1) lookup - drain_ready_join_results: now_or_never() prevents panic starvation - dispatch_heartbeat: at-most-one-globally guard - Unified SIGINT+SIGTERM shutdown via watch channel - Grace period drain + JoinSet::shutdown() abort - rx_and_join_set() split-borrow helper for select! All 116 tests pass.
Steps 5-6: Add structured tracing events for pool health monitoring and update README with parallel agents + heartbeat documentation. - 7 new structured log events (agent_claimed, agent_returned, heartbeat_fired, heartbeat_skipped_*, dispatch_pending, pool_exhausted) - has_session_for() helper in pool.rs for affinity_hit detection - README: new flags table, config examples, shared identity note, heartbeat semantics, choosing N guidance - Fix dead_code warnings in pool.rs
…-heartbeat * origin/main: feat: NIP-29 native compatibility — standard nostr clients can chat on Sprout (#63)
join_set.join_next() returns None immediately when the JoinSet has no tasks. Without a guard, the biased select loop spins at 100% CPU in the idle state (no in-flight prompts). Add is_empty() precondition so the branch is disabled when there are no tasks to join.
Convenience recipes to launch a goose agent connected to a Sprout relay. Accepts relay URL, agent count, system prompt, private key, and API token. goose-bg runs in a detached screen session.
- README: update stale 'How It Works' for multi-agent semantics - pool.rs: debug_assert slot empty in return_agent - pool.rs: remove dead next_result/result_rx_mut methods - main.rs: pool_exhausted log warn→debug (normal under load) - justfile: unique screen session name, add heartbeat param
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
Run up to N agent subprocesses in parallel with optional periodic heartbeat prompts. Adds
just gooseconvenience recipes for launching agent harnesses.N=1preserves fully backward-compatible behavior — no config changes needed for existing deployments.What Changed
pool.rsmain.rsqueue.rsHashSet), retry throttle,requeue_preserve_timestampsconfig.rs--agents,--heartbeat-interval,--heartbeat-prompt,--heartbeat-prompt-fileREADME.mdjustfilejust goose(foreground) +just goose-bg(screen session) recipesacp.rs#[allow(dead_code)]on pre-existing unusedTimeoutvariant (clippy fix)Architecture
5-branch
biased;select loop:JoinSet+task_mapwithJoinError::id()→ O(1) agent identification and recoverywatch::channel, grace period +JoinSet::shutdown()Key Design Decisions
AcpClientis not Clone. Agent moves out of slot on claim, back on return. NoArc<Mutex<>>.rx_and_join_set()split-borrow — polls result channel and JoinSet in one select without double-borrowing the poolHashSet<Uuid>ensures the same channel is never processed by two agents simultaneouslyHashMap<Uuid, Instant>with 5s backoff on failed channels, prevents tight retry loops!join_set.is_empty()guard — prevents 100% CPU spin when no tasks are in flight (JoinSet returnsNoneimmediately when empty)Box<PromptResult>in PoolEvent — clippylarge_enum_variant: PromptResult (~528B) vs JoinError (~24B), boxed to avoid bloating the enumConfiguration
--agentsSPROUT_ACP_AGENTS1--heartbeat-intervalSPROUT_ACP_HEARTBEAT_INTERVAL0--heartbeat-promptSPROUT_ACP_HEARTBEAT_PROMPT--heartbeat-prompt-fileSPROUT_ACP_HEARTBEAT_PROMPT_FILETesting
Follow-Up (deferred from review)
AgentPool::try_claim/return_agent/live_counttask_idthroughPromptResultfor explicit invariant checking