Channels, the gateway and schedules
Put a session in a chat thread, keep one resident bingo per data directory, and defer or repeat a turn.
Channels
A chat platform is a surface like any other (ADR-0016). bingo-channels holds
one deliverer and a set of adapters; each adapter hands over the mechanisms it
actually implements — editing a message, buttons, a typing indicator, threads —
so a capability can never be advertised by an adapter whose renderer does not
draw it.
Two adapters exist today: loopback, an in-process adapter with configurable limits that is the contract every behaviour is proven against, and feishu, for a self-built (企业自建) Feishu app. The long connection Feishu offers is not available to marketplace apps.
Listening
bingo channels # listen on the configured channels and nothing else
bingo --channels loopback # listen beside an ordinary run
bingo --channels loopback=127.0.0.1:7777
--channels is repeatable, and the channels setting says the same thing.
Configuring one
The app’s name is settings; what it signs with is not.
{
"channels": {
"feishu": { "appId": "cli_xxx" }
}
}
The secret comes from BINGO_FEISHU_APP_SECRET (with BINGO_FEISHU_APP_ID for
the id), or from the credential store for a gateway that inherits no shell.
Two commands write them:
bingo channels add feishu # asks for the app id and the secret together
bingo channels secret feishu # pastes a secret on its own, for rotation
What a conversation becomes
A private chat is a session. A group engages only when the bot is mentioned. A
thread continues its chat’s session. Session keys are
<adapter>/<chat>[/<thread>], minted by this plugin and nobody else, and
Origin.principal is the platform’s own user id, stamped by the adapter and
never read out of the message text.
A streaming answer is redrawn with the whole accumulated text rather than a delta, coalesced on a sentence boundary, then on a count of new characters, then on a timer — with one pending snapshot per conversation, so a stale redraw can never overwrite the final text.
A permission question renders as native buttons where the adapter has them, and
otherwise as a numbered list you answer by replying with the number or the
label. When the question is resolved anywhere — in the terminal, on the wire,
by anyone — the delivered message is edited: the buttons are stripped and the
outcome is appended (approved in the TUI). No live button outlives its
question.
One process per app
Feishu’s long connection is cluster-mode and Telegram’s polling mutually evicts, so a second bingo against the same credentials would be a silent half-outage. The adapter takes a lock file per credential under the data directory and refuses loudly rather than degrading quietly.
The gateway
An inbound door has no grace period: people write at any hour, and the Feishu
connection has no replay. bingo channels runs that door in the foreground and
dies with the terminal, so bingo gateway runs one resident bingo per data
directory and manages it like a service (ADR-0020).
bingo gateway start # spawn it detached, wait until it is up
bingo gateway status # whether one is running here, as what, and since when
bingo gateway logs -n 100 # the log, and where it is
bingo gateway stop # ask it to stop, wait for its locks to come back
bingo gateway restart
bingo gateway doctor # read settings, credentials and every lock; say what to do
bingo gateway doctor --fix # remove exactly the locks whose process is gone
bingo gateway run # the resident process itself, in the foreground
The gateway is a whole host, not a bridge: it assembles the ordinary plugin host with the channels surface listening, and sessions, transcripts, schedules and locks are the normal ones in the normal places. Nothing is proxied.
| path | what |
|---|---|
~/.bingo/data/gateway/gateway.pid | pid, binary version and start time |
~/.bingo/data/gateway/gateway.log | stdout, stderr, and the tracing sink |
install keeps it alive across logins:
bingo gateway install
bingo gateway uninstall
On macOS that writes ~/Library/LaunchAgents/com.bingo.gateway.plist and loads
it with launchctl; on Linux, ~/.config/systemd/user/bingo-gateway.service
enabled with systemctl --user. The unit runs <current exe> gateway run and
carries no secrets. While a service is installed the verbs delegate to the
supervisor, so launchd and a hand-spawned process never fight over one pidfile;
status and doctor name which mode is in force.
install, start and restart preflight the channels first — unparseable
settings, no configured channel, or a channel that cannot sign is refused with
the doctor’s own lines. A configuration that cannot run is never handed to a
supervisor to crash-loop under.
A Linux user unit stops at logout unless lingering is on; doctor has a row
for that and prints the loginctl enable-linger command rather than running
it. Windows is out of scope.
Schedules
bingo-schedule defers and repeats turns (ADR-0019). An entry is one JSON file
under ~/.bingo/data/schedules/<id>.json, hand-editable, one entry per file.
The spec is a small grammar rather than cron:
every 45s · every 30m · every 2h
daily at 09:00
once at 2026-09-01T09:00:00-07:00
Days are not a unit in every: a day is a civil thing that daylight saving
makes longer or shorter, which is what daily at is for.
A fire is a turn on the schedule’s own session, keyed schedule/<id>, at the
entry’s working directory. The transcript is the record — results, failures and
costs land where every other turn’s do, and --resume reads them. A once at
entry disables itself after firing.
The model creates and forgets them with ScheduleCreate, ScheduleList and
ScheduleForget; a person reads the table with /schedule. An unattended run
uses the entry’s own permission mode, and a question nobody answers is declined
the way a headless run declines them — the turn continues or fails honestly.
Two honest limits. Schedules fire only while a bingo process runs — the
terminal surface, bingo serve, bingo channels or the gateway. There is no
daemon of their own, and /schedule says as much on its last row —
schedules: held by this process, or schedules: dormant — held by pid 42.
And one runner per store: the first process to take the lock
file holds them, and a second runs with schedules dormant and one notice saying
who has them. On start, an entry that is overdue fires once, never once per
missed interval.