@excitedjs/dreamux
v0.27.0
Published
Dreamux core host for multi-dispatcher agent orchestration through provider interfaces.
Readme
@excitedjs/dreamux
The dreamux host server package. One long-running Node process hosts N
Dispatchers; each Dispatcher binds one or more Channel providers, one Agent
Runtime provider selected through agents[], and Dreamux-owned MCP surfaces for
channel replies, Teams, and TeamMate work.
This file is the package-level quick start. For the monorepo layout and
knowledge base, see the top-level README.md and
.agents/root.md.
Design background: #1 Proposal · #2 Engineering plan · #4 Monorepo + harness · #18 Global bin onboarding · #36 MVP tracking · #71 Capability Registry · #98 compatibility stance · #110 plugin/provider Epic.
What this package ships
- Public CLI bin:
dreamux. It owns onboarding, serving, status, doctor, dispatcher commands, and config commands. TeamMate and Team orchestration are available through Dreamux-owned MCP surfaces. - Bundled Dreamux skills injected at runtime by role (issues #209 and #313):
core hands the Dispatcher
dispatcher-workflowanddreamux-maintenance, the TeamLeaderteamwork, and both roles the shareddynamic-workflowskill. The runtime applies those role-specific plus shared roots to its engine (Codexskills/extraRoots/set, Claude Code--add-dir). Role-specific roots remain disjoint, while the shared root is deliberately composed into both roles. For admin-created TeamLeaders, required-source normalization protects bothteamworkanddynamic-workflowfrom custom skill-root shadowing.dreamux onboarddoes not install bundled skills into dispatcher workspaces. - Providerized dispatcher declarations, a process-local provider registry,
server-owned state/log paths, the
builtin:feishuChannel provider, thebuiltin:codexandbuiltin:claude-codeAgent Runtime providers, and role-gated MCP shims for channel replies, Teams, TeamMates, and cron. - Server-hosted TeamMate and Team ledgers with asynchronous turn delivery, recovery reads, bounded retry, and cold provider-native transcript retrieval.
Current Contract
- One Node process, many Dispatchers.
dispatchers[].channels[]accepts multiple channels with unique dispatcher-local ids. A dispatcher may declare at most one channel per provider ref, and each channel provider validates its own config (issue #209 multi-channel config). Live routing runs one session per declared channel and routes inbound/egress by(channel_id, target_key). - Provider refs are explicit. Wired builtin refs are
builtin:feishu,builtin:codex, andbuiltin:claude-code. Externalnpm:refs are loaded through the same provider package loader when the package is installed and implements the selected provider kind. - One dispatcher is one trust domain. A bot may receive multiple chats, but all accepted messages share one dispatcher runtime context. Do not bind unrelated private chats to the same dispatcher.
- Dispatcher cwd is explicit. Codex-backed dispatchers use Codex's global
default home (
~/.codex) for auth, memory, config, and native sessions. Claude Code-backed dispatchers use Claude Code's own CLI/auth/session behavior. Bundled skills are injected at runtime by role (not written into the dispatcher cwd). - Inbound state is in memory. The server keeps only process-local turn queues, message dedupe, and coalescing state. Restarting the server drops unprocessed inbound messages.
- Outbound is MCP reply-only. Assistant text emitted by a runtime is never
forwarded to a channel automatically. The model must call provider-specific
channel MCP tools such as
replyorreact, and those tools exist only when the Channel provider exposes the capability. - Routing belongs to the Channel. Which conversation reaches which Team is decided and stored by the Channel provider that owns the conversation, and is changed through that provider's own MCP tools. Dreamux Core has no binding table and no Collaboration Space container; a Channel names a Team when it submits, and Core takes it from there.
- TeamMate is server-hosted.
spawnstarts a named, semi-resident TeamMate and returns its concrete name;sendsubmits follow-up turns and reopens a closed TeamMate.lastreads the provider's recent Activity Records — bounded and pageable, including a turn that is still running — without starting a runtime;historyremains an identity/lifecycle recovery view. Native session identifiers and transcript paths are never published. - No webhook surface in the current contract. Feishu inbound uses the SDK long-connection WebSocket path. Webhook-only verification/encryption fields are not part of the config schema.
Explicitly not in the current contract: per-chat threads, durable inbound buffers, automatic assistant-text outbound, HTTP MCP listeners by default, reaction ledgers, streaming outbound, cross-machine coordination, and a web UI.
Install / build / test
Use the monorepo (rush) path from the repo root. It is the only supported
install path because this package depends on workspace packages through the
pnpm workspace:* protocol:
node common/scripts/install-run-rush.js update
node common/scripts/install-run-rush.js build
node common/scripts/install-run-rush.js testThe bin launcher shells out to plain node against compiled dist/ output;
no tsx is needed at runtime.
Run the server
./packages/dreamux/bin/dreamux serveThe launcher works from any cwd and via symlinks.
The server keeps operator-edited config separate from server-owned state and logs:
| Path | Purpose | Source of truth |
|---|---|---|
| dreamux config path (normally ~/.dreamux/config.json) | User-editable provider config and local channel credentials, created by dreamux onboard; edit and restart to apply | the operator |
| ~/.dreamux/run/admin.sock | Admin Unix socket (+ admin.sock.lock); volatile run file | the server |
| ~/.dreamux/run/restart-intent.json | One-shot daemon restart marker; volatile run file | the server |
| ~/.dreamux/run/sockets/ | Fallback root for ephemeral Codex app-server rendezvous sockets (preferred root: $XDG_RUNTIME_DIR/dreamux/sockets/); random per start, never persisted | the server |
| ~/.dreamux/state/<id>/access.json | Dispatcher-local Feishu access state: schema/runtime ledger are Channel-owned; policy fields and quiesced allow_users maintenance are operator-authorized | mixed |
| ~/.dreamux/state/<id>/identity.json | Dispatcher root identity plus native runtime session/transcript checkpoint | the server |
| ~/.dreamux/state/<id>/teammate/<name>/identity.json | Dispatcher TeamMate identity plus native runtime session/transcript checkpoint | the server |
| ~/.dreamux/state/<id>/team/<team-id>/ | Team record, TeamLeader identity, Team members, and Team-scoped Workflow state | the server |
| ~/.dreamux/cache/<id>/spill/ | Over-budget teammate completion spill files; rebuildable cache, only the path is inlined into a dispatcher turn | the server |
| ~/.dreamux/cache/<id>/ | Per-dispatcher provider cache root; providers own subdirectories such as Feishu attachment cache | the server |
| ~/.dreamux/cache/claude-code/ | Claude Code MCP config and skill adapters; rebuildable provider cache | the server |
| ~/.dreamux/logs/codex-app-server/<id>.log | Codex app-server stdout/stderr | the server |
| ~/.dreamux/logs/channel/<id>.log | Per-dispatcher channel logs | the server |
| ~/.dreamux/logs/teammate-mcp/<id>.log | TeamMate MCP shim diagnostics | the server |
| ~/.codex/ | Codex global default home: auth, memory, and config | the operator / Codex |
~/.dreamux/run/ and ~/.dreamux/cache/ are rebuildable while no server is
running. Durable ~/.dreamux/state/ is not a generic cleanup target; preserve
it unless an exact state owner and documented recovery contract say otherwise.
Dreamux does not create, read, validate, or repair per-entity turn.jsonl
files. Any such file left in a current entity directory is inert residue: its
presence, contents, permissions, or parseability never block startup or a
lifecycle operation, and no migration or cleanup is required.
Configure dispatchers
For normal installs, run dreamux onboard. It writes ~/.dreamux/config.json
with mode 0600, creates state/log directories, and registers a user-level
service when supported. Bundled Dreamux skills are injected at runtime by role,
not written into the workspace.
Dispatcher declarations live in config.json:
{
"agents": [
{
"id": "flow",
"provider": "builtin:codex",
"config": {
"bin": "codex",
"approval_policy": "never",
"sandbox_mode": "workspace-write",
"extra_args": [],
"extra_env": {
"EXAMPLE_FLAG": "1"
},
"initialize_timeout_ms": 10000,
"turn_timeout_ms": 600000
}
}
],
"dispatchers": [
{
"id": "flow",
"cwd": "<WORKSPACE>",
"enabled": true,
"channels": [
{
"id": "primary",
"provider": "builtin:feishu",
"config": {
"app_id": "<APP_ID>",
"app_secret": "<APP_SECRET>"
}
}
],
"agentRuntime": "flow"
}
]
}dreamux onboard is provider-ref driven. Its interactive default selects
builtin:codex and builtin:feishu, but provider-specific config prompts are
owned by those provider packages. Non-interactive runs pass provider raw config
with --agent-config-json and --channel-config-json. Operator-owned config is
never silently rewritten: old
dispatchers[].feishu / dispatchers[].codex shapes fail loudly with rebuild
guidance, following issue #98.
There is no top-level codex block and no inline dispatchers[].runtime block.
Runtime settings live in named agents[] entries and a dispatcher selects one
with dispatchers[].agentRuntime. For builtin:codex, every config field
defaults, so any field can be omitted:
bin→"codex"(resolved onPATH)approval_policy→"never"sandbox_mode→"workspace-write"extra_args→[]extra_env→{}initialize_timeout_ms→10000turn_timeout_ms→600000(accepted and defaulted by the current config reader, but not passed intoCodexRuntime; it currently has no runtime effect)
Most operators never touch bin or initialize_timeout_ms. The optional
CODEX_HOST_CODEX_BIN environment variable is a host-level override of the
codex binary across every dispatcher (e.g. CI or a non-PATH install); when
unset, the selected agents[].config.bin is used.
Claude Code agents use a different runtime-owned config shape:
{
"agents": [
{
"id": "claude",
"provider": "builtin:claude-code",
"config": {
"bin": "claude",
"model": null,
"permission_mode": null,
"remote_control": false,
"extra_args": [],
"extra_env": {},
"turn_timeout_ms": 600000
}
}
]
}Claude Code runs as a resident stream-json process (claude --print with
stream-json input/output) and receives Dreamux MCP servers through a generated
MCP config file. Set remote_control to true on a named Claude Code agent to
enable Claude Code Remote Control for every dispatcher or TeamMate launched
through that agent runtime; Dreamux logs the returned Remote Control URL through
the runtime diagnostics log when Claude Code provides one. Remote Control is an
external Claude UI control surface, distinct from Dreamux send steering. If
get_capabilities reports steer.supported: true for a Claude Code runtime,
that describes Dreamux multi-send input semantics, not Remote Control. Dreamux
does not own or attribute spontaneous turns initiated from the Remote Control UI
in this release; avoid driving external UI turns and Dreamux turns concurrently.
It does not use Codex app-server, Codex handshake, or Codex home diagnostics.
Provider refs reserved for future external providers look like npm package refs
or package export refs, for example npm:@example/dreamux-provider and
npm:@example/dreamux-provider#channel. Dreamux does not install packages for
you; if the package is already resolvable, config load imports it and validates
the provider contract before starting dispatchers.
Edit and restart dreamux serve to apply dispatcher declaration changes.
Channel ids must be unique within a dispatcher, and each dispatcher may declare
at most one channel per provider ref. Feishu app_id uniqueness is not enforced
by core; sharing one bot identity across dispatchers is an operator choice.
Dispatcher ids use a path-safe character set so they map one-to-one to state
directories.
Access-gate allowlists are not part of config.json. The complete secure V3
default used to initialize a missing ~/.dreamux/state/<id>/access.json is:
{
"version": 3,
"dm_policy": "pairing",
"group": {
"policy": "follow-user",
"allow_chats": [],
"require_mention": true
},
"allow_users": [],
"pending": {}
}access.json remains version 3. version is Channel/schema-owned.
dm_policy, group.policy, group.allow_chats, and
group.require_mention are operator policy. allow_users is shared authority:
live pairing/Owner approval may append it, and a quiesced operator may maintain
it. pending is a Channel-owned runtime ledger field. Add real chat or sender
ids only through the quiesced field-specific maintenance workflow below; the
secure default grants neither chat nor sender authority.
For human group messages, group.require_mention runs first and block drops
all human traffic. Under allowlist, an unlisted chat drops and a listed chat
trusts every exactly classified human. Under follow-user, a listed chat has
the same trust; an unlisted chat follows the existing dm_policy /
allow_users / pairing path. Trusted chats bypass dm_policy, allow_users,
and pairing, including dm_policy: "disabled", but never bypass the global
mention switch. /introduce stays sender-scoped and still requires
allow_users.
This meaning changes in place when the new server starts; V3 needs no rebuild.
Before deploying, review every non-empty allow_chats entry under both
allowlist and follow-user. Keep only groups whose human membership should
be trusted and whose passive known-bot observation should remain enabled.
The access path is always ~/.dreamux/state/<id>/access.json;
DREAMUX_ROOT is the one relocation variable for both config.json and
access.json; dreamux config path still names config.json only. For a
manual access edit, stop the daemon, confirm the process exited,
re-read after stop, apply only the requested policy/shared-authority fields via
an owner-only sibling temporary file and atomic replacement at mode 0600,
validate the complete current V3 shape without printing values, and then start
the daemon. Preserve version and the pending ledger field exactly. Never
edit the file while the daemon runs: the running Channel holds the file in
memory and its next write (a pairing request or an owner approval, for example)
replaces the whole file, discarding the edit.
A missing file after confirmed stop is valid current state: start from the full
secure V3 default shown above, create a missing state directory at 0700, and
atomically create the first 0600 file. This is initialization, not a rebuild.
dreamux config show, dreamux status, dreamux doctor, and logs redact
secret-like config keys generically. There is no CLI raw mode for printing the
unredacted local file. dreamux doctor is not an access-state validator.
Codex configuration precedence
The codex binary path resolves in this order, highest first:
CODEX_HOST_CODEX_BINenvironment variable (optional host-level override).- The selected
agents[].config.bin(default"codex").
All other Codex values come from the selected agents[].config field when
agents[].provider is builtin:codex, falling back to the built-in Codex
provider defaults. There is no global codex layer. A dispatcher's
extra_args are its only source of -c key=value options; dreamux appends its
own Channel provider MCP and TeamMate MCP -c args after them, relying on
Codex's last-write-wins behavior. Per-dispatcher extra_env is merged over the
server process environment before spawning that dispatcher app-server; dreamux
still removes CODEX_HOME so Codex keeps using its global default home.
The managed-service unit does not pin CODEX_HOST_CODEX_BIN; it adds
provider-declared binary directories from provider diagnostics to the unit
PATH, so selected provider config such as agents[].config.bin resolves.
Existing units installed before this change may still carry the env var — there
it keeps acting as the override and nothing breaks.
MCP surfaces
Each dispatcher injects Dreamux-owned MCP stdio servers into its selected Agent
Runtime provider. Codex receives runtime-specific mcp_servers.* arguments;
Claude Code receives a runtime-owned MCP config file.
The Channel provider contributes its provider-specific MCP server. For
builtin:feishu, the stdio shim does not read Feishu secrets. It serves the
provider's static tools/list metadata and forwards tools/call to the serve
process over the admin socket, where the live channel session handles the tool.
This is also the binding surface: builtin:feishu exposes bind_channel,
unbind_channel, and list_bindings, plus its own collaboration-space policy
tools, because only that provider knows what a chat, a topic, and a parent
group are.
The model-facing channel tools are supplied by the active Channel provider. Use
the provider's tools/list metadata as the current authority; the generic
Dreamux contract is that assistant text is not sent to a channel automatically.
For builtin:feishu, provider-owned examples include reply, react, and
list_chat_bots; these are Feishu provider tools, not generic Dreamux channel
tools.
The Dispatcher Service also contributes Dreamux-owned TeamMate, Team, and cron MCP servers. Dispatcher-facing TeamMate tools are:
spawn: start a resumable TeamMate and submit the first turn.send: submit a follow-up turn, reopening a closed TeamMate when resumable.close: close a named TeamMate and retain its history.list,status,history,last,get_capabilities: inspect and recover TeamMate state by concrete name.
last accepts limit (default 20, range 1 through 200), an opaque backward
pagination cursor, and include_tools. It returns provider-neutral assistant
messages and tool records oldest first, with next_cursor and truncated. Tool
arguments and tool output are never exposed, and it publishes no native IDs or
filesystem paths.
The same server also carries the Workflow tools workflow_run,
workflow_status, workflow_stop, and workflow_list.
Ordinary members of a Team and of a Dispatcher's root collection receive
exactly teammate.send and teammate.list. The roster separates the actual
owner { name, status } from compact peer rows, excludes the caller, and includes
closed peers. Send addresses that concrete owner name or a peer in the same
collection. The host identifies the sender with <task from="member-name">;
owner-issued task envelopes retain their existing form. Members cannot spawn,
close, or reach another collection.
A send returns { name, status, error? } at admission, without waiting for a
response. An idle target wakes; busy input follows the runtime's native
admission behavior (Codex steering, Claude queuing). A closed peer reopens the
same record and recorded native session. Replies require explicit sends across
turns. Sending to the owner creates no automatic reply obligation to the member;
a peer's results still go to that peer's actual owner.
Each provider-observed normal or failed native result from an ordinary member reports once to its owner, including autonomous turns with no host submission. Folded inputs share one report; separate native turns keep separate reports. The notice says that a turn completed or failed, rather than asserting goal success. Reports use the live owner runtime and do not wake an unavailable owner, so unavailable owners can refuse or drop them. Explicit messages use ordinary wake behavior. Workflow-held members have neither these tools nor ordinary native reports; their results remain script-consumed.
AgentService owns both native subscriptions and requested reply reporting, with one set of started deliveries to join during close/host stop. Its accepted-input records keep only receipt IDs, selected values and settled Promises; their reply bindings retire when the input selects an outcome. Native subscription eligibility is read synchronously at publication; requested and input-only notification eligibility is read in the settlement reaction. Requests enter the delivery policy in that same reaction, including its aggregate fence check and queue entry; the previous additional Promise hop is removed. A later dispatcher fence does not retract already-queued delivery, while a fence before queue entry drops it. This is a deliberate queue-entry timing change, not byte-identical scheduling. Recipient preparation/submission fences still apply.
Managed TeamLeaders have requested replies only. An explicit Dispatcher
team.create prompt or team.send receives its answer once per shared native
result. Team member reports, Channel requests, autonomous leader turns and later
continuations acquire no Dispatcher subscription. A create with no prompt has no
artificial answer. This boundary preserves management reads and lifecycle state.
Owner-ended inputs are suppressed before observation. Interrupted background work and connection loss create no new native completion. Bound Codex interruptions retain their selected partial/null/codec-failed values and requested notices; Claude retains stopped. Completion-less interruption/transport notices can interleave with native reports according to settlement continuations. Normal/failed native reports preserve terminal finalization order.
Dispatcher-facing Team tools are create, send, list, status, history,
and dissolve. A TeamLeader receives only dissolve, scoped to its own Team.
dissolve answers with a submission receipt: the Team is stopped and closed
behind it, and dirty or unmerged work in a managed worktree leaves the Team open
instead. Cron tools are cron_create, cron_list, cron_update, and
cron_delete.
There is no dispatcher-facing complete tool. Completion ingest is a
server/admin seam, so a dispatcher model cannot fake a TeamMate completion.
Built-In Feishu Verification Example
This example uses the built-in Feishu Channel provider; other providers use their own config and visible-reply tools.
dreamux onboard --dispatcher-id flow --dispatcher-cwd <WORKSPACE> --agent flow=builtin:codex --agent-config-json flow='{"bin":"codex"}' --channel primary=builtin:feishu --channel-config-json primary='{"app_id":"<APP_ID>","app_secret":"<APP_SECRET>"}'dreamux servestarts dispatcherflow.- Invite the bot to a Feishu group, send a mention that passes the access gate.
- Dreamux renders the inbound into a
<channel source="feishu" …>block from the neutral pieces the Channel supplied, and hands the runtime final text. - The runtime calls the Feishu channel MCP
replytool; the reply is delivered to Feishu. - Send another accepted message from a different chat in the same trust domain; it enters the same dispatcher runtime context.
- Ask the dispatcher to spawn or send TeamMate work through the
teammateMCP tools; completion delivery later arrives through the runtime-specific TeamMate completion path. - Restart the server and continue chatting; Codex
thread/resumerestores the thread when possible, but in-flight inbound messages are not durable. TeamMate final results already recorded in the ledger remain pull-able.
Testing
node common/scripts/install-run-rush.js testtests/channel-service.test.ts,tests/channel-input-format.test.tsandtests/entity-turn-owner.test.ts— Channel lifecycle, neutral inbound rendering and turn ownership with controlled provider fixtures.tests/bin-launcher.test.ts— real launcher behavior from arbitrary cwd and through symlinks.tests/doctor-plugins.test.ts,tests/onboard.test.tsandtests/uninstall.test.ts— provider diagnostics, onboarding, existence-only uninstall previews and recursive removal of the whole Dreamux root, including other files inside it. Preview changes no files and does not enumerate root contents; actual filesystem removal errors still propagate. After path normalization, a removal path inside or containing a provider-derived operator home is refused before changing the service. Physical symlink aliases are not resolved.tests/official-provider-entries.test.ts,tests/plugin-loader.test.tsandtests/provider-ref.test.ts— package defaults, plugin contributions and configured provider references.tests/teammate-completion-lifecycle.test.ts,tests/completion-delivery-owners.test.tsandtests/mcp-delegate-catalog.test.ts— actual service completion owners and MCP tool reachability, with controlled runtimes.tests/codex-live.test.ts— installed-version and real app-server initialization/thread compatibility with an empty home and no model auth. Missing Codex fails unlessDREAMUX_SKIP_LIVE_CODEX=1explicitly excludes live checks for an environment without Codex.tests/codex-inbound-live.test.ts,tests/codex-native-contracts-live.test.tsandtests/codex-activity-live.test.ts— six native model cases for issue #63 non-blocking inbound, effort, resume, structured output and live activity. These requireDREAMUX_RUN_LIVE_MODEL_GATE=1and usable model auth in both local and CI runs. The default auth-free run explicitly excludes them and does not certify their contracts. The inbound gate uses real Codex and MCP with fake Feishu platform IO. Provider constructors and defaults use public package entries. Native RPC, protocol types, version/initialization probes and Feishu bot IO interception retain test-only internal imports because no public instrumentation seam exposes these facts. Provider changes must preserve those live assertions.../agent-runtime/claude-code/tests/contains Claude runtime, session and RPC tests using synthetic protocol and subprocess fixtures. These tests do not certify authenticated Claude model behavior.
License
MIT.
