feat(ambient): implement ambient mode batch flush dispatcher#1217
Merged
Conversation
Implements the core Ambient Mode feature as described in the ADR (docs/adr/ambient.md). This adds passive channel listening with a batch flush strategy — messages accumulate in a per-channel buffer and are flushed to the LLM when a time or count trigger fires. Key components: - AmbientConfig: [ambient], [ambient.pool], [ambient.discord] sections - AmbientDispatcher: per-channel mpsc + consumer task management - ambient_consumer_loop: timer/count flush triggers with ±20% jitter - FlushingGuard: RAII + safety timeout for AtomicBool flag - PostGuard: atomic check-and-post to prevent TOCTOU double-reply - Global semaphore (max_concurrent_flushes) for cost control - [NO_REPLY] sentinel detection (is_no_reply helper) Integration: - Discord Handler routes non-mentioned messages in ambient channels to AmbientDispatcher instead of dropping them - @mention in ambient channel discards buffer + cancels in-flight flush - Reactions suppressed for ambient dispatches - Separate session pool (ambient:discord:<channel_id>) Fail-safe defaults: - enabled = false (must opt-in) - channels = [] (explicit allowlist required) - allow_bot_messages = false (prevents echo loops) - flush_timeout_seconds = 120 (auto-recover from stuck state)
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
- F1: Consumer now drains rx buffer when post_guard is cancelled, preventing stale messages from flushing on next cycle - F2: Add unit tests for is_no_reply and PostGuard - F3: display_name.clone() → .to_owned() for clarity
- F1 Critical: Move post_guard.reset() AFTER the can_post() check. Previously reset() cleared cancellation before checking it, making discard_buffer() a no-op in the race window. - F2: Add doc note that [ambient.pool] config is parsed but not yet enforced (v2 follow-up). - F4: Mark is_flushing() as #[allow(dead_code)] with v2 note.
- flush_interval_seconds=0 no longer panics (gen_range on empty range) → clamped to .max(1) at runtime - flush_max_messages=0 no longer defeats batching → .max(1) guard - flush_hard_cap=0 no longer panics (mpsc::channel(0)) → .max(1) - flush_timeout_seconds clamped to [5s, 600s] to prevent lockout - Document [NO_REPLY] not yet wired as accepted v1 limitation - Document shared DispatchTarget (tool access) as accepted v1 risk - Document config location deviation from ADR #1211
- Move post_guard.reset() to loop start (after first msg received). Fixes permanent block where one cancellation disabled all future flush cycles for that channel. - Remove dead AmbientMessage.sender_json field (never populated). - Flow: reset → accumulate → check(1) → build → check(2) → dispatch Both checks catch mid-cycle cancellations; reset prevents stickiness.
Semaphore::new(0) would block all flush operations permanently. Apply .max(1) to ensure at least one permit is always available.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Messages buffered after a @mention but during semaphore wait are valid for the next batch cycle. Draining them caused silent data loss in the narrow window where semaphore is saturated + mention arrives + new non-mention messages enter the buffer. Now: cancel discards the current batch only; remaining buffered messages naturally enter the next cycle after reset().
This comment has been minimized.
This comment has been minimized.
Introduces AmbientCaptureAdapter — a ChatAdapter wrapper that: 1. Forces non-streaming mode (use_streaming = false) so the full response text is collected before send_message is called. 2. Intercepts send_message/send_message_with_reply to check is_no_reply() and suppress the sentinel before it reaches Discord. This prevents the [NO_REPLY] sentinel from being posted to the channel, which was the expected common-case behavior (most ambient batches will produce no-reply since the channel is mostly idle chat). Without this fix, ambient mode would spam [NO_REPLY] as literal messages on every flush where the agent has nothing to contribute.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
- Explain why ambient.rs is separate from Dispatcher (no trigger_msg, no streaming, no Lane mode, NO_REPLY filtering needs differ). - Mark context_window as 'not yet implemented (v2)' in config doc.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
- docs/ambient.md: complete guide (config, behavior, limitations, example) - docs/config-reference.md: add [ambient] section - docs/discord.md: add Ambient Mode section with quick-start
Collaborator
Author
|
LGTM ✅ — Well-engineered ambient mode implementation with robust concurrency safety and fail-safe defaults. What This PR DoesImplements passive channel listening ("ambient mode") that buffers non-@mention messages per-channel and flushes them as a batch to the LLM on time/count triggers. The agent autonomously decides whether to respond; How It Works
Findings
Baseline Check
What's Good (🟢)
|
thepagent
approved these changes
Jun 27, 2026
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
Implements the core Ambient Mode feature — passive channel listening with a batch flush strategy. Messages in configured channels accumulate in a per-channel buffer and are flushed as a batch to the LLM when a time or count trigger fires.
Based on ADR: #1211
Key Design Points
mpsc::channel+ consumer task (lazy spawn)flush_interval_seconds ± 20% jitter) OR count (flush_max_messages)[NO_REPLY]sentinel — agent replies exactly this when it has nothing to add; response is discardedFlushingGuard(RAII + safety timeout) prevents permanent channel lockout on panicPostGuard(atomic cancel) prevents TOCTOU race between ambient flush and @mention dispatchSemaphore(max_concurrent_flushes = 3) controls LLM costambient:discord:<channel_id>)Configuration
Files Changed
crates/openab-core/src/config.rs—AmbientConfig,AmbientPoolConfig,AmbientDiscordConfigcrates/openab-core/src/ambient.rs—AmbientDispatcher, consumer loop, guardscrates/openab-core/src/discord.rs— Route non-mentioned msgs to ambient, discard buffer on @mentioncrates/openab-core/src/lib.rs— Module declarationsrc/main.rs— ConstructAmbientDispatcherfrom configWhat is NOT in this PR (v2 follow-ups)
[NO_REPLY]filtering before post (currently relies on system prompt)context_windowhistory fetch from Discord APImin_flush_interval_seconds)