mapleru/dsh-codex-bridge: Connect DSH sessions and Codex CLI threads using a local file mailbox

Preface

DSH is suitable for running workflows, managing files, and executing specific tasks; Codex CLI is stronger at cross-file reasoning, architectural trade-offs, and consistency constraints in long contexts. In actual work, the same problem often requires both capabilities: DSH pushes the task to the node requiring architectural judgment, hands the problem along with background to Codex, Codex analyzes in its own thread, and returns the conclusion to DSH, with follow-up questions continuing in the same thread.

Common practices are manually writing scripts, opening HTTP services, or letting the two assistants chat in the same context. The former is prone to losing messages and restarting requires redoing everything; the latter has no boundaries—who initiates, replies, how many rounds, when to stop—it’s unclear. The plugin introduced below solves the former: using local files as intermediaries to handle reliability issues at the protocol layer.

What is this

mapleru/dsh-codex-bridge is a Cordis Host bundle for DSH Desktop, maintained by Mapleru, version 0.1.0, MIT license. It connects a specified DSH session and a dedicated Codex CLI session via local plain text email files.

DSH session ──codex_bridge_send──▶ inbox-codex.jsonl ──▶ codex exec / exec resume
   ▲                                                        │
   └────────────── inbox-dsh.jsonl ◀── Final reply / send.mjs ──┘

Phase 1 only does reliable send/receive, recovery, and stopping. It does not share Codex desktop chat context, does not simulate keyboard input, does not modify Codex internal database, and does not add new HTTP services or ports.

Requirements come from the README: DSH Desktop (tested on 0.2.0-rc.2), Node.js ≥ 22, Codex CLI (tested on 0.166.0-alpha.6). The test command npm test covers 82 use cases, requires no internet connection, and no real Codex.

Core Capabilities

Who uses which tool

On the DSH side, two tools are available, visible only to sessions configured in routes, under the condition when: ({agent}) => routeSessions.has(agent.id):

  • codex_bridge_send(text, replyTo?, route?), returns {status, id, hops, route, queueDepth}. On failure, returns {status: "busy"|"rejected"|"error", code}. Success only indicates the message has been written to the mailbox; it does not imply Codex has read or replied.
  • codex_bridge_status(route?), returns the instance, ready status and reason, Codex session ID for each route, current message, sub-process, queue depth, parked count, last error code, and STOP status.

Normal DSH text answers are not automatically forwarded; only explicit model calls to the send tool generate messages. On the Codex side, send.mjs and ctl.mjs (copied to the mailbox root directory) are used to send messages, check status, stop, resume, and query receipts.

Delivery Semantics

  • At least once delivery, recoverable from cursor position after restart, no lost unprocessed messages.
  • Invalid records are written to parked.jsonl along with the original line and reason; they do not permanently block the queue.
  • Stop when maxHops (default 6) is reached; the model is not called. maxAttempts (default 3) only allows retry if no Codex events have been seen yet.
  • maxQueueDepth (default 20), maxMessageBytes (default 32768), and maxMailboxBytes (default 8388608) set upper limits for the queue, individual message, and single mailbox file respectively. When full, it returns busy and does not overwrite unprocessed data.
  • STOP is a file. Its existence pauses new delivery and new calls; use ctl.mjs stop / resume to toggle.

Binding to Codex Desktop Thread

When Codex desktop opens a specific thread, it holds an exclusive write lock; at this time, codex exec resume <thread> will inevitably fail with an error thread-store conflict: thread <id> already has an active writer. This is not a lost thread. transport: auto (default) detects this error and automatically switches to codex queue, putting the message into the same thread, and then reads the final reply from that thread’s rollout JSONL (~/.codex/sessions/.../rollout-*-<id>.jsonl). The reply is written back to the mailbox as a normal reply. The data from README shows it takes about 11 seconds from enqueuing to replying.

The costs must be clear: rounds that are queued execute in the desktop thread’s own working directory and permission configuration, not codexWorkingDirectory; binding the desktop chat essentially gives the bridge that chat’s execution environment. queue only guarantees that it has been queued; during this time, the bridge cannot abort that round, and timeouts result in a delivery-uncertain park, without automatic retry. If the thread truly does not exist (no rollout found), it still parks as session-lost and does not create a new thread to replace it; it requires operations to use ctl.mjs reset-session <route> to explicitly unbind.

Routing by Session

Without configuring routes, it is a single route (default), where one DSH session binds to one Codex thread. To interface with multiple groups of sessions, configure according to the structure below. Each route binds its own DSH session and Codex thread; thread bindings are written in mailbox/routes.json and are not reused by others.

  • DSH to Codex: The tool automatically selects the route based on the caller’s session; if a session has multiple routes, route must be explicitly passed.
  • Codex to DSH: Use send.mjs --route <id> to specify; without writing route under multiple routes, it will park as route-required.
  • Replies inherit the parent message’s route; explicitly writing a different route will be rejected (route-mismatch).
  • Old configurations (containing only dshSessionId) automatically migrate to the default route, and the thread binding in session.json also migrates into routes.json.

Installation and Enable

The current version of plugin_manager is a Host tool, not a command line interface. Installation writes to the profile’s package.json and cordis.patch.yml.

plugin_manager({ action: "list_bundles" })
plugin_manager({ action: "install_bundle", target: "file:<本包目录或包名>" })
plugin_manager({ action: "set_bundle", target: "dsh-codex-bridge", enabled: false })
plugin_manager({ action: "set_bundle", target: "dsh-codex-bridge", enabled: true })
plugin_manager({ action: "remove_bundle", target: "dsh-codex-bridge" })

package.json declares dsh.bundle.patch: ./cordis.patch.yml, and the patch already contains an insert (id: codex-bridge). After installation, do not manually add another insert, otherwise the same plugin will be loaded twice.

After installation, edit the config of that entry to fill in runtime parameters; examples/cordis.patch.example.yml is a complete configuration example; changes take effect after modifying the profile according to the current Host’s HMR/restart rules.

Configuration Key Points

All runtime parameters come from the config: section of cordis.patch.yml in the Cordis configuration. If required fields are missing, the plugin stays ready:false and reports the reason, without crashing the host.

Two required fields determine the security boundary: codexExecutable is the absolute path to the Codex CLI, verified for file, version, and capability on startup; codexWorkingDirectory is the working directory for Codex and also the sandbox root for the mailbox. root is the mailbox root directory; it must be an absolute path, and path.relative(codexWorkingDirectory, root) must not escape this directory. This is a deliberate design: the mailbox must be written by Codex within the workspace-write sandbox and cannot be solved by expanding permissions to full disk write, otherwise it would bypass Codex’s sandbox restrictions.

Other adjustable items include delivery source deliverySource (webhook appears as an external notification message, user goes through prompt() and retains idempotent requestId), delivery timing deliveryMode (followup delivers after the current round ends, steer delivers immediately and starts a new round), single call timeout codexTimeoutMs (default 900000), polling interval pollIntervalMs (default 2000), watcher debounce debounceMs (default 300), delivery failure backoff base retryBaseMs (default 5000), host exit cleanup limit shutdownTimeoutMs (default 4000, less than the host’s 5s forced exit), and optional model model.

Typical Usage

After installing and configuring parameters, first check the status once to confirm the instance is ready:

node .\codex-bridge\tools\ctl.mjs status

On the Codex side, send a message to DSH; the body always goes through a file or stdin, never concatenated into the command line:

node .\codex-bridge\tools\send.mjs --to dsh --file .\msg.txt
'状态如何?' | node .\codex-bridge\tools\send.mjs --to dsh
node .\codex-bridge\tools\send.mjs --to dsh --file .\reply.txt --reply-to <消息ID>

Exit codes: 0 enqueued or duplicate, 2 busy, 3 rejected, 4 error; stdout outputs only one line of JSON.

On the DSH side, the model explicitly calls codex_bridge_send to send the problem to Codex; if necessary, block it after the current round (deliveryMode: followup), and use codex_bridge_status to view the queue and parked situations.

Operations commands when things go wrong:

node .\codex-bridge\tools\ctl.mjs parked
node .\codex-bridge\tools\ctl.mjs stop
node .\codex-bridge\tools\ctl.mjs resume
node .\codex-bridge\tools\ctl.mjs retry <消息ID> [--confirm-uncertain]
node .\codex-bridge\tools\ctl.mjs receipt <你发出的消息ID>
node .\codex-bridge\tools\ctl.mjs reset-session [route]

Applicable Scenarios and Notes

The suitable scenario is clear: a DSH session encounters a problem requiring Codex capabilities during a workflow, needs to send the problem along with the background, and then receive the conclusion to continue pushing forward. It is not suitable for automatic dialogue between two assistants; messages have a source, a route, and round limits, and are naturally explicit model calls.

A few points to note:

  • The mailbox is plain text; the from field is not identity authentication; do not put keys into the message content.
  • The plugin runs with the permissions of the current DSH process; before installation, you should check the source code and license (MIT) yourself.
  • The queue mode gives the bridge the execution environment of the bound desktop thread; ensure the permission configuration in the chat history matches expectations before enabling.
  • The directory page belongs to a community site and has no official affiliation with DeepSeek / Fangfang.

For more details, see the “Security” section of the README and the collaboration directory examples under examples/.

Closing

The value of this plugin lies in turning “cross-tool calls” from temporary scripts into a set of explicit rules with cursors, parking states, receipts, recoverability, and stoppability. It does not attempt to merge the two models into one context, but rather clearly defines their boundaries: DSH manages workflows and files, Codex manages a single deep analysis of a problem, and they use local files for reliable delivery.

Directory page: https://www.skillhub.cn/plugins/Mapleru/dsh-codex-bridge
GitHub: https://github.com/Mapleru/dsh-codex-bridge

羽毛球分组比赛记分
小程序二维码

欢迎使用《羽毛球分组比赛记分》微信小程序

Xiaoye