Skip to content

feat(connectors): slash commands and no more silent replies on a bound chat - #650

Open
djpentz wants to merge 2 commits into
andrewyng:mainfrom
djpentz:feat/chat-commands
Open

feat(connectors): slash commands and no more silent replies on a bound chat#650
djpentz wants to merge 2 commits into
andrewyng:mainfrom
djpentz:feat/chat-commands

Conversation

@djpentz

@djpentz djpentz commented Sep 7, 2026

Copy link
Copy Markdown

The problem

An inbound message on a bound chat has three possible fates today:

  1. it carries an [ow:<id>] tag and resolves an Inbox item,
  2. it goes to the designated DM session, or
  3. it is recorded in unrouted.json and the sender is told nothing.

Three is the common case, and it is what a person meets when they reply approve
without the tag, type help, or want to know what is waiting. From the phone, silence
is indistinguishable from a dead bot.

Verbatim from the unrouted.json of the deployment this comes from — one owner, one
morning, no reply to any of it:

{"text": "approve",          "reason": "no DM session designated"}
{"text": "Help",             "reason": "no DM session designated"}
{"text": "Say something!",   "reason": "no DM session designated"}

Resolutions are silent too. Answering a mirrored prompt from a chat sends nothing back,
so always — whose entire purpose is to stop a routine asking again — gives no sign it
worked, and the person is left checking the app to find out.

The change

Slash commands, answered in the chat they came from, dispatched before routing —
"what is waiting on me?" is a question about the bot, not something to hand an agent.

/pending   what is waiting, whose it is, and the exact lines to answer it with
/status    workers, routines and their last outcome, plus the queue
/runs      how recent scheduled runs went
/help      the list

A leading slash is optional, an @botname suffix is stripped, and each has synonyms
(waiting, queue, history) — that is what people type.

Nothing is met with silence. An unmatched message replies with what is waiting
and the exact line to send. /pending prints the three answers ready to copy, so
nobody has to assemble always [ow:5f00907543c0…] by hand.

Answers acknowledge themselves, including the awkward truth: when always_task
is refused for a tool that cannot carry a standing grant, the reply says "approved
once, it will ask again" rather than letting the API's {"ok": true} imply otherwise.

Design

coworker/connectors/commands.py is pure: it reads through a ChatContext of four
callables handed in by the manager. No server needed to test it, and it cannot reach
anything it was not given.

Tests

tests/test_chat_commands.py — ten cases covering each command, the tolerant parsing,
the empty queue, questions versus approvals, and both unmatched-text paths.

Full suite: 1962 passed, 1 skipped. (Three deselected locally — they bind the fixed
OAuth callback port, which a container on this machine holds; unrelated.)


🤖 Generated with Claude Code

https://claude.ai/code/session_01PgGyV9GDu4oLhonom37BU5

An inbound chat message had three fates: it matched a prompt's `[ow:id]` tag, it went
to a designated DM session, or it was filed as unrouted and the sender told nothing.
The third is the common one, and it is what a person meets when they reply "approve"
without the tag, ask for "help", or want to know what is waiting. Silence there is
indistinguishable from a dead bot — the owner of the deployment this came from typed
"approve", then "Help", then "Say something!", and got nothing back three times.

Slash commands, answered in the chat they came from, before any routing — "what is
waiting on me?" is a question about the bot, not something to hand an agent:

  /pending  what is waiting, whose it is, and the exact lines to answer it with
  /status   workers, routines and their last outcome, plus the queue
  /runs     how recent scheduled runs went
  /help     the list

A leading slash is optional and an @botName suffix is stripped, because that is what
people type. Anything unmatched now gets a reply naming what IS waiting and how to
answer, instead of vanishing.

Answers acknowledge themselves too. Resolving a prompt from a chat said nothing back,
so "always" — whose whole point is to stop the asking — gave no sign it had worked.
It now reports what happened, including the case where a standing grant is refused
for a tool that cannot hold one, which the API otherwise reports as success.

The commands read through a narrow injected view (ChatContext), so they are testable
without a server and cannot reach anything they were not handed.
The adapter registered `MessageHandler(filters.TEXT & ~filters.COMMAND, ...)`, so
every message beginning with "/" was dropped before anything saw it — and no
CommandHandler was registered either. A bot with no command surface at all, and no
way to add one without this line changing.

The failure mode is what makes it worth a test: plain text arrives normally, so the
bot looks alive, while "/help" and "/status" vanish with no reply and no record.
Indistinguishable, from the phone, from a dead bot.

The distinction is Telegram's `bot_command` entity rather than the leading slash,
which is why the regression test builds a Message carrying that entity and asserts
both directions: the filter we now use accepts it, the one we replaced does not.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant