Skip to content

Code Wiki

An architecture reference for code readers and maintainers. This page covers system boundaries, startup chains, module responsibilities, key types and functions, and dependency direction. For user-facing documentation, see the Guide and Reference sections.

Mental model

xacpx is a "message channel ↔ command router ↔ acpx session driver" bridge:

  • Inbound: Messages arrive from WeChat, Feishu, CLI, etc. Each conversation is identified by a chatKey.
  • Router: Parses slash commands (/ss, /use, /cancel, …) and plain text. Commands dispatch to handlers; plain text becomes a prompt to the current session.
  • Sessions: Maintains a mapping from logical sessions (alias / agent / workspace / context / reply mode) to transport sessions (acpx named sessions).
  • Transport: Abstracts ensureSession / prompt / cancel / setMode uniformly. Two concrete implementations:
    • acpx-cli — spawns acpx directly (with optional node-pty PTY allocation).
    • acpx-bridge — isolated bridge subprocess + JSONL protocol; stronger concurrency and event handling.
  • Orchestration (optional): Under a coordinator session, manages task delegation to multiple worker sessions — progress reporting, human confirmation, group fan-out/fan-in.
  • Daemon: Background process lifecycle (start / status / stop). Maintains PID, status, log metadata and hosts the orchestration IPC server.
  • MCP (optional): Exposes orchestration capabilities as an MCP stdio server to external hosts (Codex, Claude Code, etc.).

Entry points

Entry pointFileFunction
CLI surfacesrc/cli.tsrunCli() — dispatches all xacpx <command> subcommands
App assembly / DIsrc/main.tsbuildApp() — wires config, state, logger, sessions, transport, orchestration, router, agent
Startup / shutdown sequencingsrc/run-console.tsrunConsole() — daemon runtime, consumer lock, channel start, finally cleanup
Command routingsrc/commands/command-router.tsCommandRouter
Session statesrc/sessions/session-service.tsSessionService
Transport boundarysrc/transport/types.tsSessionTransport interface

App assembly and startup lifecycle

buildApp() is the dependency injection center — it assembles config, state, logger, sessions, transport, orchestration, router, and agent into an AppRuntime: src/main.ts

runConsole() owns the startup sequence, signal-driven shutdown, and cleanup consistency: src/run-console.ts

  1. buildApp(paths) assembles the runtime.
  2. In daemon mode: write daemon runtime metadata, start the orchestration IPC server, start the heartbeat.
  3. Acquire the consumer lock (prevents multiple processes consuming the same WeChat account simultaneously).
  4. channels.startAll(...) — parallel channel startup.
  5. finally: stop IPC / dispose / stopAll / release lock.

Command routing

Data flow (WeChat to acpx)

  1. Channel receives a message (chatKey + text + optional media).
  2. ConsoleAgent.chat() calls router.handle(chatKey, input, reply, replyContextToken, accountId, media).
  3. CommandRouter.handle():
    • /-prefixed input: parseCommand() dispatches to the appropriate handler.
    • Plain text: treated as a prompt, resolved to the current session, forwarded to transport.prompt().
  4. Transport executes:
    • acpx-cli: spawns acpx ... prompt and aggregates output.
    • acpx-bridge: sends a JSONL request to the bridge; the bridge handles scheduling and writes back events.
  5. Reply flows back to the channel (stream / verbose / final, depending on the configured reply mode).

Key components

  • parseCommand() — slash command parser with alias resolution (/ss/session, /ws/workspace, /stop/cancel): src/commands/parse-command.ts
  • CommandRouter — thin router + context assembler; also handles transport call observation, auto-repair, and diagnostic summaries: src/commands/command-router.ts
  • Handlers — split by responsibility boundary: session lifecycle, shortcut creation, recovery, reset, config commands: src/commands/handlers/
  • router-types.ts — explicit RouterResponse, CommandRouterContext, and session ops interfaces: src/commands/router-types.ts

See Commands Module for the full module description.

Session model

Two session concepts

Logical session (xacpx-managed) — alias / agent / workspace plus persisted state (replyMode, modeId, context, etc.). Managed by SessionService and written to state.json:

Transport session (acpx-managed) — the transportSession string used as the underlying acpx named session name. ResolvedSession is the complete routing context passed to transport (includes cwd, agentCommand, transportSession, …): src/transport/types.ts

Core data concepts

  • chatKey — stable conversation identifier, globally unique across channels. Format: <channelId>:<channel-internal-id>. The channel registry uses it to route outbound messages: src/channels/channel-registry.ts
  • replyMode — xacpx reply strategy (stream / final / verbose), stored on the logical session.
  • modeId — underlying agent mode (e.g. codex plan), stored on the logical session.
  • Orchestration objectscoordinatorSession, workerSession, task, group. Assembly point in buildApp(): src/main.ts

Transport layer

Unified interface

ts
// src/transport/types.ts
interface SessionTransport {
  ensureSession(session: ResolvedSession, opts?): Promise<void>;
  prompt(session: ResolvedSession, text: string, opts?: PromptOptions): Promise<void>;
  cancel(session: ResolvedSession): Promise<void>;
  setMode(session: ResolvedSession, modeId: string): Promise<void>;
  hasSession(session: ResolvedSession): Promise<boolean>;
}

Two implementations:

  • acpx-cli (src/transport/acpx-cli/) — spawns acpx as a child process; optionally allocates a PTY via node-pty.
  • acpx-bridge (src/transport/acpx-bridge/) — talks to a separate bridge subprocess over a JSONL protocol. Better for concurrency and event isolation. See the Bridge subsystem section for the subprocess and protocol details.

acpx resolution order

  1. transport.command in config (explicit override).
  2. Bundled acpx from the main package's node_modules.
  3. acpx in shell PATH.

Channels

The core ships only the built-in weixin channel plus the generic channel/plugin infrastructure. Feishu, Yuanbao, and all other non-WeChat channels are plugin-backed and live in packages/channel-* or external npm packages — not in src/channels/.

Channel interfaces

  • MessageChannelRuntime — login / start / send / task notification: src/channels/types.ts
  • MessageChannelRegistry — aggregator that starts all channels in parallel (partial failure allowed; total failure throws) and routes outbound by chatKey: src/channels/channel-registry.ts

ConsoleAgent

ConsoleAgent is the channel-to-router adapter: it normalizes media, rejects empty messages, logs, and calls router.handle(...). Channels depend only on WechatAgent behavior, not on CommandRouter internals: src/console-agent.ts

Built-in WeChat

src/weixin/ is the built-in WeChat provider (login, polling, media pipeline, quota management), hosted by WeixinChannel in src/channels/weixin-channel.ts.

Channel capability: native session list format

/ssn native session list rendering format is declared per channel via MessageChannelRuntime.nativeSessionListFormat ("cards" | "table", default "table"; weixin declares "cards"). The registry exposes nativeSessionListFormat(chatKey), injected by CommandRouter into CommandRouterContext.resolveNativeSessionListFormat and read by the native-session handler. New channels declare this capability on the runtime — no handler changes needed.

Daemon subsystem

See Daemon Module for the full description.

DaemonController — external control surface (CLI calls):

  • getStatus() — PID missing → stopped; PID present, process gone → cleans up runtime files; PID present, no status → indeterminate.
  • start() — spawn detached → write PID → poll status.json for readiness (PID match).
  • stop() — terminate → wait for exit → clean PID and status.

Source: src/daemon/daemon-controller.ts

The daemon combines three signals to determine liveness: the PID file, whether that PID's process actually exists, and whether status.json has been written. All runtime file paths are centralized in src/daemon/daemon-files.ts.

Bridge subsystem

The bridge isolates acpx driving into a separate subprocess, giving the main process a more controllable concurrency and event channel. It backs the acpx-bridge transport implementation.

Entry and runtime

JSONL protocol

  • Methods: ensureSession / hasSession / prompt / setMode / cancel / removeSession / ...: src/transport/acpx-bridge/acpx-bridge-protocol.ts
  • Message kinds: request / response plus event (prompt.segment, session.progress, session.note).
  • Strict one-JSON-line-per-message protocol: the main process can receive session.progress and prompt.segment as events. prompt.text may be an empty string only when media is present.

Server scheduling

BridgeServer.handleLine() takes one JSON line in and writes one JSON line out; errors are uniformly wrapped as a BridgeErrorResponse. Session-scoped requests (SESSION_SCOPED_METHODS) form a scheduleKey from [agentIdentity, cwd, name] and serialize per key. cancel runs on a higher-priority control lane so it preempts an in-flight prompt. Source: src/bridge/bridge-server.ts

Configuration and state

Default paths (from resolveRuntimePaths())

PathContentWritten by
~/.xacpx/config.jsonagents, workspaces, channels, plugins, transport — static configConfigStore, CLI
~/.xacpx/state.jsonsessions, chat contexts, orchestration stateDebouncedStateStore (50 ms merge) → StateStore
~/.xacpx/runtime/daemon.pidCurrent daemon PIDDaemonRuntime
~/.xacpx/runtime/status.jsondaemon heartbeat / start_at / log pathsDaemonRuntime
~/.xacpx/runtime/app.logBounded application log (rolling)AppLogger
~/.xacpx/runtime/orchestration.sockUnix socket (or \\.\pipe\xacpx-orchestration-<hash> on Windows)OrchestrationServer
~/.xacpx/plugins/Plugin npm home (isolated package.json + node_modules)xacpx plugin add/update

WEACPX_CONFIG and WEACPX_STATE environment variables override the config and state paths respectively.

Responsibility boundary

  • config — user-explicit settings (transport, channels, agents, workspaces, logging, orchestration parameters, …).
  • state — runtime state (sessions, chat contexts, orchestration state machine data, …).

See the Configuration reference and /config Command for full field documentation.

Logging

AppLogger — structured events with local rolling file:

  • Created by createAppLogger({ filePath, level, maxSizeBytes, maxFiles, retentionDays }).
  • Rotates at maxSizeBytes using .1/.2/... suffixes; cleans files beyond maxFiles.
  • Retains by retentionDays.

Source: src/logging/app-logger.ts

State persistence

DebouncedStateStoreStateStorewritePrivateFileAtomic (proper-lockfile for cross-process mutual exclusion + write-file-atomic for atomic rename + Windows EBUSY fallback): src/state/

MCP stdio server

xacpx mcp-stdio starts an MCP stdio server and exposes orchestration tools:

This mode requires the daemon to be running (the orchestration IPC endpoint must be available). The live MCP server name exposed to external hosts is xacpx (tool prefix mcp__xacpx__*).

Released under the MIT License.