Skip to content

Commit 071a00d

Browse files
authored
feat(channels): Telegram Guest Mode and Bot-to-Bot communication (Bot API 10.0) (#3748)
Implement two Bot API 10.0 features for zeph-channels: Guest Mode (#3729): bot responds to @mentions in any Telegram chat without membership. A local bidirectional axum proxy intercepts getUpdates traffic, injecting "guest_message" into allowed_updates and forwarding guest_message updates to the agent via a shared mpsc channel. Responses are routed through answerGuestQuery (one-shot, no editMessageText). Access control checks allowed_users before any LLM call. System prompt is annotated with guest context when is_guest_context is set on ChannelMessage. Bot-to-Bot communication (#3730): bot can receive and respond to messages from other Telegram bots. Calls setManagedBotAccessSettings at startup when bot_to_bot = true. Loop prevention uses dual-check: structural reply chain depth (spec FR-007, walk reply_to_message bounded by max+1) plus consecutive bot-reply counter per chat (defense-in-depth). Default max_bot_chain_depth=1 to reflect the Telegram API payload's single nesting level. New config fields under [telegram]: guest_mode = false bot_to_bot = false allowed_bots = [] max_bot_chain_depth = 1 New ChannelMessage fields: is_guest_context, is_from_bot. All 31 construction sites across the workspace updated. 9 new unit tests in zeph-channels (9089 total, +10 vs main). Closes #3729, Closes #3730
1 parent 7242b68 commit 071a00d

33 files changed

Lines changed: 1031 additions & 58 deletions

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
2828
Unblocks Guest Mode (#3729), Bot-to-Bot communication (#3730), and reaction moderation (#3731).
2929
Closes #3728.
3030

31+
- feat(channels): add Telegram Guest Mode and Bot-to-Bot communication (`zeph-channels`, `zeph-config`).
32+
Guest Mode spawns a transparent local axum HTTP proxy on an ephemeral port that intercepts
33+
`getUpdates` responses from `api.telegram.org`, extracts `guest_message` entries (which
34+
teloxide-core 0.13 discards as unknown update kinds), and forwards them to the agent — without
35+
a second `getUpdates` connection (no 409 Conflict). The `Bot` is redirected to the proxy via
36+
`Bot::set_api_url()`. Responds via `answerGuestQuery` in a single call from `flush_chunks`;
37+
`send_chunk()` accumulates text only in guest context (NFR-005 compliance). Bot-to-Bot mode
38+
registers via `setManagedBotAccessSettings` on startup; `bot_to_bot_active` is set only on
39+
success via `Arc<AtomicBool>` (remains false if API call fails). Per-chat consecutive bot reply
40+
depth tracked via `BotReplyCounters`; eviction removes one random entry at capacity (not
41+
bulk-clear). New config fields: `telegram.guest_mode`, `telegram.bot_to_bot`,
42+
`telegram.allowed_bots`, `telegram.max_bot_chain_depth`. Guest context propagated via
43+
`ChannelMessage.is_guest_context` → `SessionState.is_guest_context` → volatile system prompt
44+
annotation. Fixed missing `is_guest_context`/`is_from_bot` fields in `gateway_spawn.rs` and
45+
`daemon.rs`. Closes #3729, #3730.
46+
3147
- feat(memory): add BeliefMem probabilistic edge layer to APEX-MEM (`zeph-memory`). New
3248
`pending_beliefs` + `belief_evidence` SQLite tables (migration 084) store candidate facts with
3349
probability weights before commitment. Evidence accumulates via Noisy-OR with optional temporal

crates/zeph-acp/src/agent/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1282,6 +1282,8 @@ impl ZephAcpAgentState {
12821282
.send(ChannelMessage {
12831283
text: text.clone(),
12841284
attachments,
1285+
is_guest_context: false,
1286+
is_from_bot: false,
12851287
})
12861288
.await
12871289
.map_err(|_| acp::Error::internal_error().data("agent channel closed"))?;
@@ -2005,6 +2007,8 @@ impl ZephAcpAgentState {
20052007
let _ = tx.try_send(ChannelMessage {
20062008
text: "/clear".to_owned(),
20072009
attachments: vec![],
2010+
is_guest_context: false,
2011+
is_from_bot: false,
20082012
});
20092013
}
20102014
"Session history cleared.".to_owned()
@@ -2067,6 +2071,8 @@ impl ZephAcpAgentState {
20672071
.try_send(ChannelMessage {
20682072
text: review_prompt,
20692073
attachments: vec![],
2074+
is_guest_context: false,
2075+
is_from_bot: false,
20702076
})
20712077
.is_err()
20722078
{

crates/zeph-bench/src/channel.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,8 @@ impl zeph_core::channel::Channel for BenchmarkChannel {
204204
Ok(Some(ChannelMessage {
205205
text,
206206
attachments: vec![],
207+
is_guest_context: false,
208+
is_from_bot: false,
207209
}))
208210
}
209211
None => Ok(None),

crates/zeph-channels/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ readme = "README.md"
1515
[features]
1616
profiling = []
1717
discord = ["dep:tokio-tungstenite", "dep:futures"]
18-
slack = ["dep:axum", "dep:hmac", "dep:sha2", "dep:subtle"]
18+
slack = ["dep:hmac", "dep:sha2", "dep:subtle"]
1919

2020
[dependencies]
21-
axum = { workspace = true, optional = true }
21+
axum = { workspace = true }
2222
crossterm.workspace = true
2323
futures = { workspace = true, optional = true }
2424
hmac = { workspace = true, optional = true }

crates/zeph-channels/src/cli.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,8 @@ async fn process_line(
135135
Ok(Some(ChannelMessage {
136136
text: trimmed.to_string(),
137137
attachments,
138+
is_guest_context: false,
139+
is_from_bot: false,
138140
}))
139141
}
140142

@@ -771,6 +773,8 @@ mod tests {
771773
tx.send(ChannelMessage {
772774
text: "hello".to_string(),
773775
attachments: vec![],
776+
is_guest_context: false,
777+
is_from_bot: false,
774778
})
775779
.await
776780
.unwrap();

crates/zeph-channels/src/discord/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ impl Channel for DiscordChannel {
162162
return Some(ChannelMessage {
163163
text: incoming.content,
164164
attachments: vec![],
165+
is_guest_context: false,
166+
is_from_bot: false,
165167
});
166168
}
167169
}
@@ -188,6 +190,8 @@ impl Channel for DiscordChannel {
188190
return Ok(Some(ChannelMessage {
189191
text: incoming.content,
190192
attachments: vec![],
193+
is_guest_context: false,
194+
is_from_bot: false,
191195
}));
192196
}
193197
}

crates/zeph-channels/src/json_cli.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ impl Channel for JsonCliChannel {
9898
return Ok(Some(ChannelMessage {
9999
text,
100100
attachments: Vec::new(),
101+
is_guest_context: false,
102+
is_from_bot: false,
101103
}));
102104
}
103105
Some(None) | None => return Ok(None), // EOF
@@ -119,6 +121,8 @@ impl Channel for JsonCliChannel {
119121
Some(ChannelMessage {
120122
text: trimmed,
121123
attachments: Vec::new(),
124+
is_guest_context: false,
125+
is_from_bot: false,
122126
})
123127
}
124128
_ => None,

crates/zeph-channels/src/slack/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ impl Channel for SlackChannel {
139139
Some(ChannelMessage {
140140
text: incoming.text,
141141
attachments: vec![],
142+
is_guest_context: false,
143+
is_from_bot: false,
142144
})
143145
}
144146

@@ -171,6 +173,8 @@ impl Channel for SlackChannel {
171173
Ok(Some(ChannelMessage {
172174
text: incoming.text,
173175
attachments,
176+
is_guest_context: false,
177+
is_from_bot: false,
174178
}))
175179
}
176180

0 commit comments

Comments
 (0)