DSH Cross-Session Relay
English | 中文
Lets multiple independent DeepSeek Harness (dsh) sessions discover each other and exchange text messages peer-to-peer.
The problem
dsh ships two built-in "cross-session" capabilities, neither of which is peer-to-peer messaging between processes:
- subagents are parent/child: the parent agent spawns a subagent that lives in its context and stays under its control — not an independent peer.
@sessionmentions in the Web UI are one-way, read-only recall: you can reference another session's history, but cannot send it a message.
In practice, though, two sessions often need to send each other messages. Take a "database assistant" and a "frontend assistant" split into two independent sessions, each in the environment that suits it:
- the database assistant runs on an intranet machine with database access and context;
- the frontend assistant runs on a dev machine and owns the UI.
When the frontend assistant needs the CREATE TABLE SQL for a users table, it
has no database context or credentials of its own, so it has to message the
database assistant; the database assistant looks it up and replies. This
peer-to-peer ask/reply across different specialties, permissions, or machines is
exactly what subagents' parent/child structure and @session's read-only recall
cannot cover. This plugin adds that peer-to-peer, two-way channel.
What this plugin does
A peer-to-peer, centerless, cross-process (optionally cross-machine) message channel:
- Model-driven: the agent itself calls
cross_session_list_agentsto discover peers andcross_session_send_messageto send; no human relays anything. - Relay injection, never authorization: messages arrive as
form: 'relay'text and never approve a tool call on the user's behalf — the receiver's own approval still applies. - Reliable delivery: sender-side spool + exponential backoff + dead-letter; receiver persists before ack; at-least-once with content dedup.
- Local and cross-machine: Unix sockets locally, TCP+TLS (fail-closed, static peers) across machines.
- Centerless: the on-disk registry is the source of truth; there is no central server.
How it differs from dsh's built-ins
| subagent | @session mention |
this plugin | |
|---|---|---|---|
| Relationship | parent/child | one-way, read-only | peer-to-peer |
| Can send messages | parent → child control | no | yes, both ways |
| Cross-process / cross-machine | same process | same process | yes |
| Injection form | subagent context | history snapshot | relay text, no authorization |
Installation
Prerequisites: Node.js >= 24.11 and a working dsh (DeepSeek Harness) environment.
When installing from source, build the artifacts first (lib/ is not distributed):
npm install
npm run build
Then add the plugin to a profile:
dsh plugin --profile demo add ./cross-session-messaging
dsh --profile demo --dump-config # verify it is wired in
The plugin is inserted into the profile at the bundle layer via cordis.patch.yml;
any fields not listed fall back to schema defaults.
Usage
Open two dsh sessions (for example, two "New Session" in the Web UI) and, in one of them, ask in natural language:
List the other sessions currently running, then send one of them a message: hello.
The model calls cross_session_list_agents to discover the peer and
cross_session_send_message to send; the other session receives the message as
form: 'relay'. The human only writes a natural-language request and names no
tool.
See Quick start for the full end-to-end walkthrough.
Configuration
Configuration lives in the plugin's cordis.patch.yml under config: (already
used to wire it in during installation); any field not listed falls back to its
schema default. Local use normally needs no customization beyond name and the
inbound policy:
- insert:
- id: cross-session-messaging
name: '@wy/dsh-cross-session-messaging'
config:
dirs:
baseDir: ~/.dsh/cross-session # data root; registry/socket/spool derive from it
inbound:
rules: [] # inbound rules, see below
defaultDecision: auto # fallback when no rule matches
name: '' # session name, defaults to the shortId
# Cross-machine (optional, see "Cross-machine"):
# remote:
# listen: { host, port, identity, secret }
# tls: { key, cert }
# peers:
# - { name, host, port, secret, tls, tlsCa/tlsFingerprint }
dirs.baseDir: root of all data directories (registry/socket/spool/credential/deadletter/audit), default~/.dsh/cross-session; each of the six can be overridden.inbound.rules:accept | hold | refuserules matched byreceiver(local session id/name) andsender(sender sessionId); omitting either means "all local sessions" / "default rule".inbound.defaultDecision: fallback when no rule matches;auto(default) consults the receiver'sDSH_PERMISSION_MODE(danger-full-access→ hold, otherwise accept), or setaccept | hold | refuseexplicitly.name: the session's readable name, used for addressing bycross_session_send_message.
Cross-machine
Cross-machine delivery needs remote (omit it for local-only use):
remote.listen: the TCP listener when this machine receives (host/port/identity/secret);identityis how this machine appears to peers and is stamped onto inboundfrom.remote.tls: listener certificate (key/cert); omit to auto-generate a self-signed cert underspool/tls, with its fingerprint written tospool/tls/fingerprint.remote.peers: the static peer list when this machine sends (name/host/port/secret/tls/tlsCaortlsFingerprint).tlsdefaults totrue; cross-machine TLS requirestlsCaortlsFingerprint, otherwise the connection is refused.
Capacity and retry (tuning)
limits.* (queue caps, message size, rate limit, dedup window), delivery.*
(retry/backoff), metricsIntervalMs, and auditMaxBytes have conservative
defaults and rarely need changing; adjust by field name when you do.
Tools
cross_session_list_agents: list reachable peer sessions (sessionId / name / shortId / cwd / pid).cross_session_send_message: send text to a session by sessionId, shortId, or name.cross_session_inbox_review: review held messages awaiting accept/refuse.cross_session_inbox_resolve:acceptorrefusea held message.
Permission semantics
- Peer messages are injected with
source.form = 'relay', enter the model as text only, and never approve any tool call on the user's behalf; the receiving tools still go through their own approval (fail-closed by default). - The
fromidentity is overwritten by server-side authentication (local = sessionId, cross-machine =listen.identity), never trusted from the sender; tool names this session's permission system rejected are remembered, sosend_messagewill not forward text asking another session to run one of them.
Hooks / child-process replies
The plugin injects the current socket / sessionId / handshake token into shell child processes via dsh's ctx.shellEnv registry (DSH_CROSS_SESSION_SOCKET / DSH_CROSS_SESSION_ID / DSH_CROSS_SESSION_TOKEN). A long-running child process or hook can connect to that socket and send back text, or call the exported sendToCurrentSession(text).
Testing
npm test # unit + integration (real sockets)
npm run check # lint + typecheck + build + test
Test layering, the cross-machine stress matrix, and CI policy are in docs/testing.md.
Limitations
- Cross-machine discovery is static
peersconfig (requires restart), with no central registry and no public NAT traversal; real public two-host transport/service layers are verified (stable at low frequency, occasional ping failures at high frequency), but the host end-to-end where the model proactively calls the tool is not yet verified. - TLS certificates are auto-generated (openssl self-signed) but not rotated/revoked; cross-machine TLS requires
tlsCa/tlsFingerprint(fail-closed). - Delivery is at-least-once with content dedup (across restarts), not strict exactly-once: duplicates are possible outside the window or for different content.
- Both accepted and held are persisted (recovered on restart); accepted messages are marked after injection and re-delivered if not injected, with a minimal crash window.
- Cross-machine identity is single-peer:
listen.identityis a single value (two-machine assumption); future multi-peer needs anallowedlist. - Forward protection is heuristic (best-effort); the real security boundary is relay-does-not-authorize + the default inbound policy.
- Peer messages are injected with
form: 'relay'and carry no user authorization; receiving tools still use their own approval (fail-closed by default).
No comments yet. Be the first to write one.