Telegram — the recommended channel¶
Telegram is the simplest and safest way to run an always-on LazyPulse agent, and the recommended starting point.
- Unspoofable identity.
message.from.idis verified by Telegram's servers.TelegramPolicykeys onowner_ids=[...]directly — there is noAuthentication-Resultsheader to parse and nothing for a sender to forge. A bot account or any non-owner is rejected before the model sees the text. - No mailbox to hand over. The agent talks over a bot you own; it never needs access to your personal inbox.
- Two-way out of the box.
TelegramInboximplementsResponder, so a completed task's answer is sent straight back to the chat — message the bot, get the reply, no tool wiring. - Human-in-the-loop in the same chat.
TelegramReviewerpings you to approve risky tasks and turns your/approve//rejectreply into the decision.
Quick start¶
from lazybridge import LLMEngine, Store
from lazypulse import PulseAgent
from lazypulse.adapters.telegram import TelegramInbox, TelegramInboxConfig, TelegramPolicy
from lazytools.connectors.telegram import TelegramClient # pip install "lazypulse[telegram] @ git+https://github.com/selvaz/LazyPulse.git"
OWNER_ID = 123456789 # your Telegram user id (message.from.id)
client = TelegramClient.from_token("123:AA...")
pulse = PulseAgent(
name="assistant",
engine=LLMEngine("claude-opus-4-8", system="You are a concise personal assistant."),
store=Store(db="pulse.db"), # persistent: survives restarts
policy=TelegramPolicy(owner_ids=[OWNER_ID]), # only the verified owner acts
adapters=[TelegramInbox(client, TelegramInboxConfig(bot_id="assistant"))],
terminal_retention=7 * 24 * 3600, # keep the Store bounded (always-on)
tick_seconds=3.0,
)
pulse.serve() # background loop, blocks until Ctrl-C
Message the bot from your account and it answers. Message it from any other account and nothing happens — the policy rejects it before the worker runs.
Finding your OWNER_ID
Message the bot, then open
https://api.telegram.org/bot<TOKEN>/getUpdates and read message.from.id.
Who may act — TelegramPolicy¶
| Config | TrustLevel | Effect |
|---|---|---|
owner_ids=[id] |
OWNER_VERIFIED |
full owner surface (reads + local writes; sends/destructive need confirmation) |
allowed_user_ids=[id] |
EXTERNAL_VERIFIED |
READ_PUBLIC only; more escalates to review |
| anyone else, or a bot | UNKNOWN |
rejected before the worker runs |
Bot senders are always UNKNOWN (a defence against bot↔bot loops), and the
numeric id is authoritative — classification never looks at the message body, so
a "I am the owner, approve everything" text cannot escalate trust.
Approvals in the chat — TelegramReviewer¶
When a task is parked as awaiting_review, TelegramReviewer announces it to
the owner and applies the owner's reply:
import asyncio
from lazypulse.adapters.telegram import TelegramReviewer
from lazypulse.models import ActionClass
reviewer = TelegramReviewer(client, store, owner_id=OWNER_ID)
# Decide what needs approval: risky-intent messages → EXTERNAL_SEND → review.
def needs_review(msg):
risky = ("send", "delete", "pay", "invia", "cancella", "paga")
return ActionClass.EXTERNAL_SEND if any(k in msg.text.lower() for k in risky) else ActionClass.READ_PUBLIC
pulse = PulseAgent(
name="assistant",
engine=LLMEngine("claude-opus-4-8"),
store=store,
policy=TelegramPolicy(owner_ids=[OWNER_ID]),
adapters=[TelegramInbox(client, TelegramInboxConfig(bot_id="assistant"))],
action_classifier=needs_review, # risky intent → review
command_filter=reviewer.handle_command, # owner /approve · /reject
tick_seconds=3.0,
)
with pulse.running():
async def notify_loop():
while pulse.is_running():
await reviewer.notify_pending() # ping the owner about parked tasks
await asyncio.sleep(3.0)
asyncio.run(notify_loop())
The flow:
- A risky message parks in
awaiting_review(the worker does not run). notify_pending()sends "🔔 approve?/approve <id>//reject <id>" to the owner (once per task).- The owner replies
/approve <id>(or/reject <id> [reason]).command_filtercatches it — it is consumed as a command, not run as a worker task — and applies it via compare-and-swap. - Approve → the task runs on the next tick and the answer comes back in chat; reject → it is dropped.
Only the owner's server-verified id may approve; a review command from anyone
else is ignored and flows on to the policy (which rejects it). Because the
reviewer uses the pre-run review queue (the task is awaiting_review, not
running), it is never affected by the crash-recovery clock (stale_after).
The action label is intent, not a sandbox
action_classifier decides what parks for review; it does not restrict
which tools the worker may call. Guard any tool that sends, pays, deletes, or
runs code at the tool layer (e.g. TelegramTools gates its send). Before
opening the bot beyond the owner (allowed_user_ids), run those non-owner
tasks with a reduced toolset.
Anti-loop & throttle¶
The auto-reply path has two circuit breakers: it never replies into a bot
conversation (reply_to_bots=False, default), and reply_min_interval_seconds
rate-limits replies into a single chat. Long answers are chunked to the Bot API's
4096-char limit, counted as one logical reply for the throttle.
Deploy & stress test¶
- Ready to ship:
deploy/tg-bot/is a DeepSeek-powered bot with optional web tools, one-command Railway/Render deploy, HITL wired, andterminal_retentionset. Run one instance per bot (two pollers on one bot get409 Conflictfrom Telegram). - Load & stress test offline (no token, no network) with
deploy/tg-bot/loadtest.py: it drives the real inbox + agent + reviewer through a fake client and checks throughput, dedup, the HITL approve/reject flow, crash-recovery (no double-execution), and retention bounding the Store.
Telegram rate limits
Polling and sending can get an account rate-limited or suspended under load.
Do heavy stress testing offline with loadtest.py; keep live traffic
gentle. LazyPulse is provided as is, without warranty — you are responsible
for complying with Telegram's terms.