Skip to content

Commit 7e223f4

Browse files
jamiepineclaude
andcommitted
rename Quiet to Observe, remove Listen Only toggle
Quiet mode is renamed to Observe with new semantics: the agent learns from the conversation (context + memory) but never responds, even when mentioned or replied to. This creates three clearly distinct modes: - Active: responds to everything - Mention Only: sees everything, responds only to mentions/replies/commands - Observe: sees and learns from everything, never responds Also removes the legacy Listen Only toggle from the Channel Behaviour config UI — response modes in channel settings fully replace it. Backwards compat: "quiet" is accepted as a serde alias and in config parsing, so existing channel_settings rows and TOML configs continue to work. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ce2e9d7 commit 7e223f4

File tree

10 files changed

+89
-88
lines changed

10 files changed

+89
-88
lines changed

docs/content/docs/(getting-started)/configuring-channels.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Controls when the agent responds to messages.
2222
| Mode | Behavior |
2323
|------|----------|
2424
| **Active** (default) | Responds to all messages normally. |
25-
| **Quiet** | Observes and learns from the conversation (history + memory persistence), but only responds when @mentioned, replied to, or given a command. |
25+
| **Observe** | Learns from the conversation passively — never responds, even when mentioned. All messages are ingested into context and memory. Use this for channels the agent should monitor without participating. |
2626
| **Mention Only** | Messages are still visible to the agent for context, but it only responds when @mentioned, replied to, or given a command. Memory capture continues passively. |
2727

2828
#### Mention Only vs. Binding-Level Require Mention
@@ -116,7 +116,7 @@ Some settings can be toggled at runtime via slash commands in chat:
116116
| Command | Effect |
117117
|---------|--------|
118118
| `/active` | Switch to Active response mode |
119-
| `/quiet` | Switch to Quiet response mode |
119+
| `/observe` | Switch to Observe response mode |
120120
| `/mention-only` | Switch to Mention Only response mode |
121121

122122
These persist to the channel's settings and survive restarts.

interface/src/api/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ export type ConversationSettings = {
134134
model_overrides?: ModelOverrides;
135135
memory?: "full" | "ambient" | "off";
136136
delegation?: "standard" | "direct";
137-
response_mode?: "active" | "quiet" | "mention_only";
137+
response_mode?: "active" | "observe" | "mention_only";
138138
save_attachments?: boolean;
139139
worker_context?: {
140140
history?: "none" | "summary" | "recent" | "full";

interface/src/components/ChannelCard.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,9 @@ export function ChannelCard({
160160
<span className="text-tiny text-ink-faint">
161161
{formatTimeAgo(channel.last_activity_at)}
162162
</span>
163-
{channel.response_mode === "quiet" && (
163+
{channel.response_mode === "observe" && (
164164
<span className="inline-flex items-center rounded-md bg-amber-500/10 px-1.5 py-0.5 text-tiny font-medium text-amber-400">
165-
Quiet
165+
Observe
166166
</span>
167167
)}
168168
{channel.response_mode === "mention_only" && (

interface/src/components/ConversationSettingsPanel.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,14 +95,14 @@ const DELEGATION_DESCRIPTIONS: Record<string, string> = {
9595

9696
const RESPONSE_MODE_OPTIONS = [
9797
{ value: "active", label: "Active" },
98-
{ value: "quiet", label: "Quiet" },
98+
{ value: "observe", label: "Observe" },
9999
{ value: "mention_only", label: "Mention Only" },
100100
] as const;
101101

102102
const RESPONSE_MODE_DESCRIPTIONS: Record<string, string> = {
103103
active: "Responds to all messages normally.",
104-
quiet:
105-
"Observes and learns from the conversation, but only responds when @mentioned, replied to, or given a command.",
104+
observe:
105+
"Learns from the conversation passively — never responds, even when mentioned. Use this for channels the agent should monitor without participating.",
106106
mention_only:
107107
"Messages are still visible to the agent for context, but it only responds when explicitly @mentioned, replied to, or given a command. To block messages entirely, use the binding-level require mention setting instead.",
108108
};

interface/src/components/WebChatPanel.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -336,9 +336,9 @@ export function WebChatPanel({agentId}: WebChatPanelProps) {
336336
?? settings.model ?? defaults.model}
337337
</span>
338338
)}
339-
{settings.response_mode === "quiet" && (
339+
{settings.response_mode === "observe" && (
340340
<span className="rounded-md bg-amber-500/10 px-1.5 py-0.5 text-[10px] font-medium text-amber-400">
341-
Quiet
341+
Observe
342342
</span>
343343
)}
344344
{settings.response_mode === "mention_only" && (

interface/src/routes/AgentConfig.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1221,12 +1221,7 @@ function ConfigSectionEditor({ sectionId, label, description, detail, config, on
12211221
case "channel":
12221222
return (
12231223
<div className="grid gap-4">
1224-
<ConfigToggleField
1225-
label="Listen-Only Mode"
1226-
description="Only respond when explicitly invoked (slash command, @mention, or reply-to-bot)."
1227-
value={localValues.listen_only_mode as boolean}
1228-
onChange={(v) => handleChange("listen_only_mode", v)}
1229-
/>
1224+
<p className="text-sm text-ink-faint">Channel behavior is now configured per-channel via response modes in Channel Settings.</p>
12301225
</div>
12311226
);
12321227
case "projects":

src/agent/channel.rs

Lines changed: 67 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -694,7 +694,7 @@ impl Channel {
694694
self.resolved_settings = resolved;
695695
}
696696

697-
/// Whether the channel is in a non-active response mode (Quiet or MentionOnly).
697+
/// Whether the channel is in a non-active response mode (Observe or MentionOnly).
698698
fn is_suppressed(&self) -> bool {
699699
!matches!(self.resolved_settings.response_mode, ResponseMode::Active)
700700
}
@@ -917,7 +917,7 @@ impl Channel {
917917
.unwrap_or_else(|| routing.resolve(ProcessType::Branch, None));
918918
let mode = match self.resolved_settings.response_mode {
919919
ResponseMode::Active => "active",
920-
ResponseMode::Quiet => "quiet (only command/@mention/reply)",
920+
ResponseMode::Observe => "observe (learning, never responds)",
921921
ResponseMode::MentionOnly => "mention-only (@mention/reply only)",
922922
};
923923
let adapter = self.current_adapter().unwrap_or("unknown");
@@ -941,11 +941,11 @@ impl Channel {
941941
self.send_builtin_text(body, "status").await;
942942
return Ok(true);
943943
}
944-
"/quiet" => {
945-
self.set_response_mode(ResponseMode::Quiet).await;
944+
"/quiet" | "/observe" => {
945+
self.set_response_mode(ResponseMode::Observe).await;
946946
self.send_builtin_text(
947-
"quiet mode enabled. i'll only reply to commands, @mentions, or replies to my message.".to_string(),
948-
"quiet",
947+
"observe mode enabled. i'll learn from this conversation but won't respond.".to_string(),
948+
"observe",
949949
).await;
950950
return Ok(true);
951951
}
@@ -975,8 +975,8 @@ impl Channel {
975975
"- /today: in-progress + ready task snapshot".to_string(),
976976
"- /tasks: ready task list".to_string(),
977977
"- /digest: one-shot day digest (00:00 -> now)".to_string(),
978-
"- /quiet: only reply to commands, @mentions, or replies".to_string(),
979-
"- /mention-only: only respond when @mentioned or replied to".to_string(),
978+
"- /observe: learn from conversation, never respond".to_string(),
979+
"- /mention-only: only respond when @mentioned, replied to, or given a command".to_string(),
980980
"- /active: normal reply mode".to_string(),
981981
"- /agent-id: runtime agent id".to_string(),
982982
];
@@ -1418,37 +1418,36 @@ impl Channel {
14181418
}
14191419
}
14201420

1421-
if !matches!(self.resolved_settings.response_mode, ResponseMode::Active)
1422-
&& !batch_has_invoke
1423-
&& !self.is_dm()
1424-
{
1421+
// Observe mode: always suppress (even with mentions in batch).
1422+
// MentionOnly mode: suppress only when no invocations in the batch.
1423+
let should_suppress_batch = !self.is_dm()
1424+
&& match self.resolved_settings.response_mode {
1425+
ResponseMode::Active => false,
1426+
ResponseMode::Observe => true,
1427+
ResponseMode::MentionOnly => !batch_has_invoke,
1428+
};
1429+
1430+
if should_suppress_batch {
14251431
tracing::debug!(
14261432
channel_id = %self.id,
14271433
message_count,
14281434
response_mode = ?self.resolved_settings.response_mode,
14291435
"suppressing unsolicited coalesced batch"
14301436
);
1431-
// In MentionOnly mode, inject batch messages into in-memory history
1432-
// so the LLM retains channel context when eventually triggered.
1433-
if matches!(
1434-
self.resolved_settings.response_mode,
1435-
ResponseMode::MentionOnly
1436-
) {
1437-
{
1438-
let mut history = self.state.history.write().await;
1439-
for (formatted_text, _, _) in &pending_batch_entries {
1440-
history.push(rig::message::Message::User {
1441-
content: OneOrMany::one(UserContent::text(formatted_text)),
1442-
});
1443-
}
1444-
}
1445-
// Compaction guard: suppressed messages accumulate in history
1446-
// without agent turns, so check compaction to prevent unbounded growth.
1447-
if let Err(error) = self.compactor.check_and_compact().await {
1448-
tracing::warn!(channel_id = %self.id, %error, "compaction check failed");
1437+
// Inject batch messages into in-memory history so the agent
1438+
// retains channel context.
1439+
{
1440+
let mut history = self.state.history.write().await;
1441+
for (formatted_text, _, _) in &pending_batch_entries {
1442+
history.push(rig::message::Message::User {
1443+
content: OneOrMany::one(UserContent::text(formatted_text)),
1444+
});
14491445
}
14501446
}
1451-
// Both Quiet and MentionOnly keep passive memory capture.
1447+
if let Err(error) = self.compactor.check_and_compact().await {
1448+
tracing::warn!(channel_id = %self.id, %error, "compaction check failed");
1449+
}
1450+
// Both Observe and MentionOnly keep passive memory capture.
14521451
self.message_count += message_count;
14531452
self.check_memory_persistence().await;
14541453
return Ok(());
@@ -1798,9 +1797,12 @@ impl Channel {
17981797
}
17991798
}
18001799

1801-
// Deterministic ping ack for Discord quiet-mode mentions/replies to avoid
1800+
// Deterministic ping ack for Discord mention-only mentions/replies to avoid
18021801
// flaky model behavior (e.g. skipping or over-formatting simple liveness checks).
1803-
if should_send_discord_quiet_mode_ping_ack(&message, &raw_text, self.is_suppressed()) {
1802+
// Skipped in Observe mode — the agent never responds in Observe.
1803+
if !matches!(self.resolved_settings.response_mode, ResponseMode::Observe)
1804+
&& should_send_discord_quiet_mode_ping_ack(&message, &raw_text, self.is_suppressed())
1805+
{
18041806
self.send_builtin_text("yeah i'm here".to_string(), "discord-ping")
18051807
.await;
18061808
return Ok(());
@@ -1848,40 +1850,41 @@ impl Channel {
18481850
let mut invoked_by_reply = false;
18491851

18501852
// Response mode guardrail:
1851-
// In Quiet/MentionOnly modes, ingest messages but only reply when explicitly invoked.
1853+
// Observe mode: always suppress — agent learns but never responds.
1854+
// MentionOnly mode: suppress unless explicitly invoked.
18521855
if !matches!(self.resolved_settings.response_mode, ResponseMode::Active)
18531856
&& message.source != "system"
18541857
&& !self.is_dm()
18551858
{
1856-
(invoked_by_command, invoked_by_mention, invoked_by_reply) =
1857-
self.compute_listen_mode_invocation(&message, &raw_text);
1859+
// Observe mode always suppresses; MentionOnly checks for invocation.
1860+
let should_suppress =
1861+
if matches!(self.resolved_settings.response_mode, ResponseMode::Observe) {
1862+
true
1863+
} else {
1864+
(invoked_by_command, invoked_by_mention, invoked_by_reply) =
1865+
self.compute_listen_mode_invocation(&message, &raw_text);
1866+
!invoked_by_command && !invoked_by_mention && !invoked_by_reply
1867+
};
18581868

1859-
if !invoked_by_command && !invoked_by_mention && !invoked_by_reply {
1869+
if should_suppress {
18601870
tracing::debug!(
18611871
channel_id = %self.id,
18621872
source = %message.source,
18631873
response_mode = ?self.resolved_settings.response_mode,
18641874
"suppressing unsolicited reply"
18651875
);
1866-
// In MentionOnly mode, inject the message into in-memory history
1867-
// so the LLM retains channel context when eventually triggered.
1868-
if matches!(
1869-
self.resolved_settings.response_mode,
1870-
ResponseMode::MentionOnly
1871-
) {
1872-
{
1873-
let mut history = self.state.history.write().await;
1874-
history.push(rig::message::Message::User {
1875-
content: OneOrMany::one(UserContent::text(&user_text)),
1876-
});
1877-
}
1878-
// Compaction guard: suppressed messages accumulate in history
1879-
// without agent turns, so check compaction to prevent unbounded growth.
1880-
if let Err(error) = self.compactor.check_and_compact().await {
1881-
tracing::warn!(channel_id = %self.id, %error, "compaction check failed");
1882-
}
1876+
// In Observe and MentionOnly modes, inject the message into
1877+
// in-memory history so the agent retains channel context.
1878+
{
1879+
let mut history = self.state.history.write().await;
1880+
history.push(rig::message::Message::User {
1881+
content: OneOrMany::one(UserContent::text(&user_text)),
1882+
});
1883+
}
1884+
if let Err(error) = self.compactor.check_and_compact().await {
1885+
tracing::warn!(channel_id = %self.id, %error, "compaction check failed");
18831886
}
1884-
// Both Quiet and MentionOnly keep passive memory capture.
1887+
// Both Observe and MentionOnly keep passive memory capture.
18851888
self.message_count += 1;
18861889
self.check_memory_persistence().await;
18871890
return Ok(());
@@ -1948,10 +1951,10 @@ impl Channel {
19481951
self.handle_agent_result(result, &skip_flag, &replied_flag, is_retrigger)
19491952
.await;
19501953

1951-
// Safety-net: in quiet mode, explicit mention/reply should never be dropped silently.
1954+
// Safety-net: in mention-only mode, explicit mention/reply should never be dropped silently.
19521955
if should_send_quiet_mode_fallback(
19531956
&message,
1954-
QuietModeFallbackState {
1957+
ObserveModeFallbackState {
19551958
is_suppressed: self.is_suppressed(),
19561959
is_retrigger,
19571960
invoked_by_command,
@@ -3626,7 +3629,7 @@ fn should_send_discord_quiet_mode_ping_ack(
36263629
}
36273630

36283631
#[derive(Debug, Clone, Copy)]
3629-
struct QuietModeFallbackState {
3632+
struct ObserveModeFallbackState {
36303633
is_suppressed: bool,
36313634
is_retrigger: bool,
36323635
invoked_by_command: bool,
@@ -3638,7 +3641,7 @@ struct QuietModeFallbackState {
36383641

36393642
fn should_send_quiet_mode_fallback(
36403643
message: &InboundMessage,
3641-
state: QuietModeFallbackState,
3644+
state: ObserveModeFallbackState,
36423645
) -> bool {
36433646
state.is_suppressed
36443647
&& !state.is_retrigger
@@ -3668,7 +3671,7 @@ fn is_dm_conversation_id(conv_id: &str) -> bool {
36683671
#[cfg(test)]
36693672
mod tests {
36703673
use super::{
3671-
QuietModeFallbackState, compute_listen_mode_invocation, is_dm_conversation_id,
3674+
ObserveModeFallbackState, compute_listen_mode_invocation, is_dm_conversation_id,
36723675
recv_channel_event, should_process_event_for_channel,
36733676
should_send_discord_quiet_mode_ping_ack, should_send_quiet_mode_fallback,
36743677
};
@@ -3896,7 +3899,7 @@ mod tests {
38963899

38973900
assert!(should_send_quiet_mode_fallback(
38983901
&message,
3899-
QuietModeFallbackState {
3902+
ObserveModeFallbackState {
39003903
is_suppressed: true,
39013904
is_retrigger: false,
39023905
invoked_by_command: false,
@@ -3908,7 +3911,7 @@ mod tests {
39083911
));
39093912
assert!(!should_send_quiet_mode_fallback(
39103913
&message,
3911-
QuietModeFallbackState {
3914+
ObserveModeFallbackState {
39123915
is_suppressed: true,
39133916
is_retrigger: false,
39143917
invoked_by_command: false,
@@ -3920,7 +3923,7 @@ mod tests {
39203923
));
39213924
assert!(!should_send_quiet_mode_fallback(
39223925
&message,
3923-
QuietModeFallbackState {
3926+
ObserveModeFallbackState {
39243927
is_suppressed: true,
39253928
is_retrigger: false,
39263929
invoked_by_command: false,
@@ -3932,7 +3935,7 @@ mod tests {
39323935
));
39333936
assert!(!should_send_quiet_mode_fallback(
39343937
&message,
3935-
QuietModeFallbackState {
3938+
ObserveModeFallbackState {
39363939
is_suppressed: true,
39373940
is_retrigger: true,
39383941
invoked_by_command: false,

src/api/channels.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ pub(super) async fn list_channels(
144144
let settings = &cs.model_overrides;
145145
let mode = match settings.response_mode {
146146
crate::conversation::ResponseMode::Active => None,
147-
crate::conversation::ResponseMode::Quiet => Some("quiet".to_string()),
147+
crate::conversation::ResponseMode::Observe => Some("observe".to_string()),
148148
crate::conversation::ResponseMode::MentionOnly => {
149149
Some("mention_only".to_string())
150150
}

src/config/load.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ fn parse_response_mode(
123123
if let Some(mode) = response_mode {
124124
return match mode {
125125
"active" => Some(ResponseMode::Active),
126-
"quiet" => Some(ResponseMode::Quiet),
126+
"observe" | "quiet" => Some(ResponseMode::Observe),
127127
"mention_only" => Some(ResponseMode::MentionOnly),
128128
unknown => {
129129
tracing::warn!(
@@ -137,8 +137,8 @@ fn parse_response_mode(
137137
// Backwards compat: listen_only_mode maps to response_mode
138138
match listen_only_mode {
139139
Some(true) => {
140-
tracing::warn!("listen_only_mode is deprecated, use response_mode = \"quiet\" instead");
141-
Some(ResponseMode::Quiet)
140+
tracing::warn!("listen_only_mode is deprecated, use response_mode = \"observe\" instead");
141+
Some(ResponseMode::Observe)
142142
}
143143
Some(false) => Some(ResponseMode::Active),
144144
None => None,
@@ -2453,7 +2453,7 @@ impl Config {
24532453
}
24542454
if let Some(r) = s.response_mode.as_deref() {
24552455
match r {
2456-
"quiet" => cs.response_mode = ResponseMode::Quiet,
2456+
"observe" | "quiet" => cs.response_mode = ResponseMode::Observe,
24572457
"mention_only" => cs.response_mode = ResponseMode::MentionOnly,
24582458
"active" => cs.response_mode = ResponseMode::Active,
24592459
other => tracing::warn!(

src/conversation/settings.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,12 @@ pub enum ResponseMode {
116116
/// Respond to all messages normally.
117117
#[default]
118118
Active,
119-
/// Observe and learn (history + memory persistence) but only respond
120-
/// to @mentions, replies-to-bot, and slash commands.
121-
Quiet,
119+
/// Observe only — never respond, even when mentioned or replied to.
120+
/// All messages are ingested into in-memory context and conversation
121+
/// history, and passive memory capture continues. The agent learns
122+
/// from the conversation but never generates a response.
123+
#[serde(alias = "quiet")]
124+
Observe,
122125
/// Only respond when explicitly @mentioned, replied to, or given a command.
123126
/// Messages that don't pass the mention check are still ingested into
124127
/// the in-memory context window (so the agent stays context-aware),

0 commit comments

Comments
 (0)