Channels let you reach the same DuoDuo installation from Feishu, an editor, a room you speak in, or the other AI assistants you already use. Each channel has its own status and configuration; credentials stay on the machine, outside chat and source control.

Feishu / Lark#
duoduo channel install @openduo/channel-feishu
duoduo channel feishu start
duoduo channel feishu status
Store the App ID and App Secret in the persistent host environment, then restart the channel process. Do not paste secrets into a conversation or commit them to the repository.
Owner DM#
The configured owner's direct message is the simplest place to begin. Its first message can bind to the default workspace automatically. After that, /setup stays locked there so the owner always has one reliable way back in.
Groups and secondary DMs#
Use /setup to connect a group or another direct message. A group can point at the workspace root or one project. For safety, a secondary DM can only choose a narrower project.
How a reply looks#
Every finished reply carries a one-line footer: tokens in and out with the cache share, context size, the model that served the turn, and the cost. Whatever a runtime does not report is simply left out and the line renders shorter. There is nothing to configure.
The process card is the optional part. It streams status while the turn runs and names the tools as they are called, so a long answer is visible work instead of silence. Choose it per channel:
feishu:
process_card: replace # off (default) | replace | keep
off— one card, written when the reply is ready.replace— the process card becomes the answer when the turn ends. One card, start to finish.keep— the finished process card stays as a trace and the answer arrives as its own card.
Put it in the kind descriptor to set every Feishu channel, or in one instance descriptor for a single chat. Both files are read on every message, so the change applies to the next one without restarting the channel. A card only ever appears for turns that channel started.
Diagnose card actions#
duoduo channel feishu stop
duoduo channel feishu doctor
duoduo channel feishu start
Use doctor for Feishu card-action failures. It checks the developer-console subscription, release, and encryption layers. The command refuses to run while the plugin process is live.
ACP editor bridge#
duoduo channel install @openduo/channel-acp
duoduo channel acp start
ACP needs no channel credentials. Each editor pane maps to one daemon session, and the editor provides the workspace directly. Closing the pane releases that session cleanly.
Ambient#
Ambient is DuoDuo in a room. Round-the-clock ears, no app to open, no button to hold, no wake word to recite before every sentence. You talk. It is already listening—and it already knows whether you were talking to it.
That last part is the whole thing, and it is what separates a room from a smart speaker. A speaker asks did this person stop talking? Ambient asks the much harder question—was that said to me?—and answers it the way someone sitting in the corner would.
Being talked about is not being talked to#
Two colleagues at a whiteboard spend four minutes arguing about whether DuoDuo handled yesterday's rollout well. Its name comes up a dozen times. It says nothing: no chime, no "did you mean me?", not half a second of a listening animation.
Then one of them turns to the corner and asks what it thinks—and it answers about the actual argument. Both positions, who said which, the rollout they were describing. It had been listening the whole time. It simply had no standing to speak until someone gave it one.
The judgement runs the other way too, which is the half most systems skip. Command-shaped speech is not automatically an order: people dictate to their phones in exactly the phrasing they would use on DuoDuo, and being overheard is not being instructed. Retelling an old conversation, thinking out loud, a rhetorical question, telling a third person what to do—someone in the room would answer none of those.
Its name is evidence, not a trigger. Hearing it counts for something; hearing it is not the same as being called.
It records either way#
Everything anyone says becomes part of the room's record, addressed to DuoDuo or not. That record is how it reads the room later. The addressee judgement decides only whether it acts—recording is not a behavioural choice.
The room stays in the room#
The hearing runs on infrastructure you host—voice presence, transcription, who-said-what, and the voiceprints that keep identities stable—so the room's audio stays on machines you run. A room full of people talking all day is the most sensitive recording there is; it belongs on your own infrastructure, not a vendor's.
Two things are yours to place. The judge that decides whether something was said to DuoDuo runs on any OpenAI-compatible endpoint: host it yourself and the room's words stay on your machines too, or point it at a hosted API and they go there. Spoken replies are synthesized by a cloud realtime endpoint, so the text of what DuoDuo says leaves for that—the room's audio does not.
Transcript and speaker attribution arrive together, so a five-person conversation is already sorted by who said what instead of one undifferentiated block, and the same person keeps one identity through the day without anyone enrolling a voice sample.
Voice presence is continuous rather than push-to-talk, so you can interrupt it mid-word. What it says next is grounded in how much of its answer actually reached you, not in the paragraph it meant to say.
A room is a directory#
Each room is an instance directory alongside the other channels, and what it knows about itself is files:
| File | Written by | What it holds |
|---|---|---|
notes.md | you and DuoDuo | What has been worked out about the room: whose voice is whose, the phrases it keeps mishearing |
transcript-*.jsonl | the runtime | What was said, as it was heard |
imlog-*.jsonl | the runtime | The cleaned record of the room |
events-*.jsonl | the runtime | What happened in the room that nobody said |
An unfamiliar voice gets a temporary badge for that room, never a name of its own accord. You give it a name by writing one line in notes.md, the way you would introduce a person. Deciding unprompted that two voices are the same person is the confident, undetectable kind of error that collapses a whole room into one identity, so the code is not allowed to make it.
There is no alias table and nothing to configure. notes.md is prose, and a misidentification—or a word the room keeps getting wrong—is corrected by editing a line of it. The file is read fresh for each exchange, so a correction applies to the next thing said: no restart, no reprocessing.
Run it yourself#
Ambient is published at openduo/ambient: the channel, the wire protocol, and the perception service behind it, under a source-available license. It is built from source, not installed from npm, and docs/deploy.md in that repository is a numbered runbook from an empty machine to a room that answers.
It needs one Linux machine with an NVIDIA card and one cloud credential, for speech synthesis; everything else is self-hosted. The judge dominates the budget, so the stack comes in two profiles: a constrained one that fits in about 4.5 GB of free video memory on a single card, and an ample one with a 27B judge across two cards. The channel runs beside the daemon; the perception service runs beside the model services it calls.
For a single RTX 4090- or 3090-class card, openduo/ambient-engine builds an optional judge server for a ternary 27B model. You do not need it: a hosted API or another server works the same way.
Configuration#
Ambient follows the same layering as the other channels: a kind descriptor for defaults across every room, an instance descriptor for one room, and the host environment for addresses and credentials. The name a room answers to, how long it waits before deciding a sentence is finished, and how much of the room's recent talk it carries into an answer all live in the descriptors—so one loud room can be tuned without touching the others.
Tether: your other assistants#

Tether lets the AI assistants you already use—ChatGPT, Claude, Cursor, Grok, an agent you built—connect to DuoDuo over MCP and share its memory and context. A connected assistant loads DuoDuo's board at the start of a chat, reads its memory and event log, mails your DuoDuo sessions and the other assistants, and records what it did. That record lands in the event log like any other experience, so what you worked out in another assistant can reach the intuition every session starts with.
duoduo channel install @openduo/channel-tether
# set ALADUO_TETHER_PORT in ~/.config/duoduo/.env
duoduo channel tether start
duoduo channel tether status
Installing and starting it is the opt-in; duoduo channel tether stop closes it.
Only you approve#
Tether is one MCP endpoint behind OAuth. Every connection needs your passkey on its authorize page, bound to the public address; no message, link, or token can stand in for it, and no agent can approve one—DuoDuo included. Enroll your passkey as soon as the public address is up: until one exists, whoever opens the enrollment link first enrolls it, and status warns about that.
You name each connection. The name is how DuoDuo knows the assistant, and duoduo channel tether list and revoke <name> are how you see and end one. Tokens do not expire; a connection lasts until it is revoked or the public address changes.
What a connected assistant can reach#
- DuoDuo's board and memory files, read-only
- the event log in an external view: human messages and DuoDuo's replies in full, tool calls only as a name and an outcome, internal events left out
- mail to and from your channel sessions and the other connected assistants
- records it writes into the event log, under its own name
It cannot run commands on the host, call the daemon's API, or read tool output. Each of those reaches needs its own OAuth scope.
A mail is a request in text, never your permission. The sender it names says where it came from, not who is allowed to do what, so any outward or irreversible step a mail asks for—sending to someone else, deleting, paying—needs your first-hand confirmation. The subconscious holds the same line: another assistant's report of a conversation with you is second-hand. What it says about you is a claim, not your ruling.
Exposing it is your choice#
Tether binds to loopback and serves plain HTTP there. Making it reachable as a public HTTPS address—your own reverse proxy, a Cloudflare tunnel, Tailscale Funnel, a relay for a host with no inbound port—is your decision and your security; Tether names no route as official and ships none. Whatever route you choose should expose the Tether channel and nothing else on the host.
The duoduo-tether skill walks an agent through the whole setup: choosing a route, enrolling your passkey, adding <public address>/mcp as a connector in the assistant, and proving the assistant answers mail on its own. The repository, with its protocol, deployment, and security documents, is openduo/tether.
Channels and upgrades#
A gateway is a separate process talking to the daemon, so it has to be upgraded alongside it. duoduo upgrade does that for you: it reinstalls each channel, restarts the daemon, and brings the channels back, in an order where a channel whose download failed keeps running its previous build rather than being left stopped.
Doing it by hand, per channel:
duoduo channel <kind> install
duoduo channel <kind> stop && duoduo channel <kind> start
A gateway left on an older build than the daemon fails quietly — the bot keeps receiving messages and answers none. If a channel goes silent after an upgrade, reinstall and restart it before looking anywhere else.
Kind and instance configuration#
- A kind descriptor sets defaults for every channel of one kind.
- An instance descriptor overrides one specific room, DM, or editor surface.
Resolve both locations with duoduo daemon config. Preserve existing frontmatter, comments, and prompt bodies when editing them.