DuoDuo runs as a daemon on your machine. The duoduo CLI tells you whether it is healthy, which configuration it is using, and what happened when something goes wrong.
Check before you change#
duoduo daemon status
duoduo daemon config
duoduo daemon logs
statusreports process and scheduler health.configresolves active paths and settings.logsshows the readable tail; request more only when diagnosis needs it.
Start with what the running daemon reports. Its effective configuration is more useful than a copied default.
What is running right now#
When tool calls are in flight, status lists each one: which session, which tool, how long ago it started. When no tool calls are open, the list is empty.
It passes no judgement. There is no threshold, no watchdog, no timeout that decides a call has taken too long—status reports, and /cancel acts. Read the empty list carefully: it means no tool call is open, which is equally true while the model is writing.
Read the event history#
duoduo spine cat --date <yyyy-mm-dd> --session <key>
duoduo spine cat --date <yyyy-mm-dd> --count-only
duoduo spine show <event-id>
The Spine WAL is the source of truth, and on a busy host a single day of it is tens of megabytes of JSONL. Read it through the CLI rather than opening the files: cat prints speech in full and folds each tool call together with its result, and show prints one event whole.
Size a window with --count-only before pulling it into an agent's context, and continue a long read with --after <event-id> rather than starting over. A cat with no narrowing filter refuses to print a body unless you pass --unfiltered.
The by-id index is a bounded recency cache, compacted at boot, so it does not grow without limit on a long-lived host. ALADUO_SPINE_INDEX_RETENTION_DAYS sets how much it keeps. Nothing becomes unreadable when an id falls out of it—the read falls back to scanning that day's partition.
Upgrade#
duoduo --version
npm view @openduo/duoduo version
duoduo upgrade
One command does the whole job. It installs into the prefix that owns the binary currently running, reinstalls and restarts the channel plugins, restarts the daemon with the version as the reason, and waits for a health check before reporting success.
The prefix part matters if duoduo was installed outside npm's default global prefix — which is how the menubar app installs it. A plain npm install -g there writes a second copy elsewhere, leaves the running binary untouched, and reports success anyway.
It is safe to run from inside a DuoDuo conversation: the work is handed to a detached process that the daemon restart cannot kill. Add --wake <session-or-alias> for a session that should be told when it is done.

For a manual upgrade in the default npm prefix, install the package and restart the daemon. Then reinstall and restart each channel as described in Channels:
npm install -g @openduo/duoduo@latest
duoduo daemon restart -r "upgraded @openduo/duoduo to <version>"
Installing a new package does not hot-swap the detached process. Restart the daemon so it runs the new code, and say what changed: the reason reaches every session that wakes afterward, and a session whose turn the restart ended has no other way to learn why.
Upgrades that cross a major boundary occasionally need a manual step. Those are called out in the release notes.
Skills upgrade separately#
The published agent skills are separate from the npm package. CLI and npm upgrades do not refresh them; Manager’s upgrade flow does. To refresh them yourself:
npx -y skills add https://github.com/openduo/duoduo --global --all
How the daemon is reached#
The daemon serves its full interface on a unix socket with owner-only permissions, so the operating system decides who may drive your agent. The CLI, the channel gateways, and the menubar app find it by default; there is nothing to configure.
127.0.0.1:20233 is a read-only surface for the dashboard. It is pinned to loopback, answers only the methods the dashboard needs, and does not speak WebSocket. Control belongs to the CLI; do not point scripts at that port.
| Setting | What it controls |
|---|---|
ALADUO_PORT | The read-only TCP port, default 20233. Always loopback. |
ALADUO_DAEMON_SOCKET | Absolute path override for the socket. Rarely needed. |
ALADUO_DAEMON_HOST + ALADUO_REMOTE_PORT + ALADUO_DAEMON_TOKEN | The opt-in remote listener. |
Reaching a daemon from another machine#
Remote access is opt-in and always carries a token.
duoduo daemon token new
The token is stored with owner-only permissions, and every remote request must carry it. The listener starts only when host, port, and token are all present, and the remote port must differ from the read-only one. A misconfiguration — a public address with no token, or a port collision — refuses to start rather than exposing anything.
Install the operations skills#
npx -y skills add https://github.com/openduo/duoduo \
--global \
--yes \
--skill duoduo-admin duoduo-channel-admin duoduo-runtime-admin
These skills teach a coding agent how to inspect, configure, recover, and manage channels for the version you actually installed. Ask for the work in plain language.
Tool access is an allowlist#
Claude sessions receive a fixed core tool surface. Extra built-in tools—such as web search or fetch—must be added in a channel kind or instance descriptor under claude.tools.
claude:
tools:
- WebSearch
- WebFetch
The list adds tools to Claude's fixed core set. Codex manages tools separately.
A scheduled job can carry the same block in its own frontmatter, with host-wide defaults in kernel/config/job.md. See Loops & Jobs.
If a tool is missing, run duoduo session config <target> get and check the configuration that session actually received.