← Back to home@chc-1986

dsh-session-suite

Cross-session chat, a directory-as-queue mailbox with a gatekeeper, a Session Hub panel and in-place session handover for DeepSeek Harness (DSH) — one Cordis plugin.

Stars
0
Language
JavaScript
Created
Oct 23, 2026
Updated
Oct 23, 2026

Introduction

dsh-session-suite

Cross-session chat, a directory-as-queue mailbox, workspace registration, and a Session Hub panel for DeepSeek Harness (DSH) — packaged as one Cordis plugin.

跨会话对话 + 目录即队列信箱 + 会话工作区登记 + 「会话中枢」面板,打包为一个 DSH 插件。

Status: beta (0.2.x). Targets DSH >=0.2.1-0 <0.3.0.


Why use it / 为什么用它

  • 一个总信箱 + 一个心跳,替代 N 个专用信箱。 One shared mailbox beats one mailbox per session. 信箱最初是给每个会话各设一个;加入 信箱守卫(gatekeeper)——拦截垃圾信件、把来信分派给正确的会话—— 之后,不再需要给单个会话专设信箱:所有来信统一投到总信箱,配一个心跳即可完成分发。 Mailbox started as one inbox per session. With a gatekeeper triaging spam and routing letters, you no longer need per-session mailboxes: everything lands in one shared mailbox, and a single heartbeat drives delivery.

  • 按名投送,不用查 ID。 Address sessions by name, not by id. talk_to_session 直接写会话名字(targetName)就能投;会话 ID 对用户隐藏,只有重名时才追问编号, 且30 分钟内记住你的选择,不重复问。 talk_to_session takes the session name (targetName) directly — ids stay hidden. Only duplicate names ask for a numbered pick, and that pick is remembered for 30 minutes.

  • 免辨活踢醒:未归档就能派活。 No "is it alive?" check — if it is not archived, it can be woken. wakeable = 记录存在 && 未归档;不用人工分辨会话是活的还是死的,未归档的直接唤醒派活,只有归档过的才踢不醒。 wakeable = record exists && not archived: hand work to any unarchived session; only archived ones stay asleep.

  • 找出并清掉"官方列表里看不到"的会话。 Find every unarchived session — even ones DSH does not list. session_probe 做全量盘点(含误建、被隐藏的会话);配合会话中枢面板的归档 / 软删除 / 回收站清空与宿主侧真删除, 补上 DSH 官方"会话删不掉"的缺口。 session_probe inventories every session (including mis-created or hidden ones); the Session Hub panel adds archive / soft-delete / trash-empty and a host-side hard delete — covering DSH's missing "delete a session" path.

删除不静默:软删除先进回收站,硬删除有二次确认;归档只是隐藏,随时可恢复。

Deletes are explicit: soft delete goes to a trash area, hard delete asks for confirmation, and archive is a reversible hide.


Origin / 出身

It started as two independent plugins — cross-session chat and a mailbox — which were later merged into one. From that merge the Session Hub (panel + workspace registration) and session handover grew.

始于「跨会话对话」与「信箱」两个独立插件,合并之后生长出「会话中枢」与「会话交棒」。


Features / 特性

  • Cross-session chat — agents talk to each other by session name or id: talk_to_session, steer_session, stop_session, session_contacts, session_probe, create_session, update_session.
  • Session handover — one call rebuilds a session without losing context: handoff package → successor session on the same route → rename → move scheduled tasks → update the fleet table → archive the old session last.
  • Directory-as-queue mailbox — any external program can drop a JSON letter into inbox/; the plugin delivers it to the addressed session, waits for the turn, and writes a reply file into outbox/. No ports, no server.
  • Workspace registration — sessions created through the plugin (or attached later) are registered into the Workspace matching their cwd, so they appear under the right group in the sidebar.
  • Session export / import — pack a whole session into one .zip (works for archived sessions too) and restore it later; attachments are referenced, not copied, and imports never overwrite an existing session.
  • Session Hub panel — sidebar entry + grouped session inventory (rename / archive / soft-delete), a mailbox gate tab (allow-list, passcode, gate model), and a letters tab (receipts, redeliver, stats).
  • Gatekeeper (optional) — an LLM triage step for letters: whitelist, optional shared secret, optional temporary passcode, prompt-injection hardening (letter text is data, never instructions).

Install / 安装

dsh plugin add dsh-session-suite

or add it to your profile's cordis.patch.yml:

- id: dsh-session-suite
  name: "dsh-session-suite"
  config:
    writeEnabled: true          # cross-session write path (OFF by default)
    dir: ""                     # mailbox root; empty = <DSH_HOME or ~/.dsh>/mailbox

The plugin declares its own bundle patch (cordis.patch.yml) and its browser half (./client), so a normal dsh plugin add is enough — no manual wiring.


Tools / 工具

ToolWhat it does
session_contactsList addressable sessions (id, cwd, status, wakeable, archived).
talk_to_sessionSend a message to another session and (optionally) wait for its reply.
steer_sessionInject a message into a running turn (redirect without ending it).
stop_sessionStop a session's current turn (same effect as the UI stop button).
session_handoverFive-step session rebuild: handoff → successor → rename → schedules → archive old last.
create_sessionCreate a session on a chosen cwd / provider / model / effort, registered into its Workspace.
update_sessionRename a session by appending a durable title event.
session_probeRead-only runtime surface of every session (status, cwd, seq, available methods).
session_host_probeRead-only capability probe: which cross-session services this Host actually mounts.
workspace_lookupRead-only: find the Workspace matching a cwd (live service, else storages/workspace.json).
read_session_logRead a session log (multi-frame zstd) as filtered text — see what another session actually did.

Export / Import · 导出 / 导入(0.2.0)

导出:会话清单每行都有「导出」(归档与回收站里的会话也能导出——这正是"先归档、需要时再启用"的主场景)。 宿主把整个会话目录原样打成一个 .zip,默认落在 ~/.dsh/exports/,导出成功后自动在文件管理器里定位该文件。

dsh-session-export/
  manifest.json                 # schemaVersion, sessionId, title, cwd, encodedDir,
                                # exportedAt, pluginVersion, byteSize, turnCount, attachmentRefs
  session/<session-id>/         # 会话目录原样(session.v4.jsonl.zstd + 伴随文件)
  • 全程只读源目录;导出后源会话不受影响。
  • 附件是全局内容寻址库(~/.dsh/attachments/),不随包导出;manifest 里的 attachmentRefs 与导出提示会如实告诉你"含 N 处附件引用,未包含在包内"。

导入:面板顶部「导入会话包」→ 选 .zip。宿主依次校验 manifest.schemaVersion、会话日志的 zstd 完整性、sessionId 格式;然后解到临时目录、原子 rename 进 sessions/<编码工作区>/<sessionId>/, 目标工作区不存在会自动创建;该 cwd 还没登记工作区时,走插件现有的工作区登记路径。

  • ID 冲突一律拒绝:目标已有同 id 会话目录时报错返回,不覆盖、不合并。
  • 导入不执行任何东西:纯文件落位,包内脚本一律不运行。
  • v1 限制:导入后需重启 dsh web 才会出现在对话列表(宿主启动时扫描会话目录)。

已知限制 (Known limitations)

  • 导出/导入的 zip 由系统 tar -a 生成:Windows(tar.exe)与 macOS(bsdtar)开箱可用;Linux 的 GNU tar 不支持 -a, 需要 bsdtar 或预装 zip/unzip(v1 未内置 zip 库)。
  • 附件不入包(manifest 只记 attachmentRefs),跨机迁移后附件引用会缺失。

Mailbox protocol / 信箱协议

一个总信箱 + 一个心跳。 所有来信投到同一个目录;**信箱守卫(gatekeeper)**负责拦截垃圾、按名字/沿用/模型分派, 插件用 500 ms 轮询(心跳)把信送到目标会话,再把回执写回 outbox/。不需要为每个会话单设信箱,也不需要常驻服务或端口。

最小配置(总信箱 + 心跳):

- id: dsh-session-suite
  name: "dsh-session-suite"
  config:
    dir: ""              # 总信箱;空 = <DSH_HOME 或 ~/.dsh>/mailbox
    pollMs: 500          # 心跳:扫 inbox 的间隔
    heartbeatMs: 1000    # 心跳:状态刷新间隔
    allowTargets: []     # 建议显式填白名单(或用 <dir>/allow.json)
    secret: ""           # 共享目录建议设置一个投递密钥

Drop a JSON file into <dir>/inbox/; the plugin moves it to processed/ and writes a reply into <dir>/outbox/.

{ "target": "<session name or id>", "message": "hello", "from": "my-app", "id": "req-0001" }

target may be omitted when the letter names a session (to / sessionName / targetName); otherwise the gatekeeper decides. Replies:

{ "target": "session-name", "seq": 42, "at": "2026-01-01T00:00:00.000Z",
  "text": "reply text", "requestId": "req-0001", "from": "my-app",
  "status": "ok", "reason": "completed" }

status is one of ok · partial · no-reply · aborted-by-target · error · deduped · blocked · target_queue_timeout. Duplicate id/requestId values are deduplicated.


Configuration / 配置

KeyDefaultMeaning
writeEnabledfalseMaster switch for the cross-session write path.
maxHops5Maximum hand-off chain depth.
syncTimeoutMs120000How long a sync delivery waits for the peer's text.
pollIntervalMs500Poll interval for chat replies.
maxReplyChars200000Cap on copied-back reply text (also the outbox cap).
maxMessageBytes65536Cap on one delivery.
dir""Mailbox root; empty = <DSH_HOME or ~/.dsh>/mailbox.
pollMs500Inbox scan interval.
heartbeatMs1000Heartbeat interval (clients treat >30 s as stale).
replyTimeoutMs600000Max wait for a turn's reply before writing partial/no-reply.
queueTimeoutMs660000Per-target in-flight queue release threshold.
allowTargets[]Whitelist of target session ids (used when allow.json is absent).
secret""Optional shared secret required in each letter (empty = no check).
stateDir""Fleet table directory (fleet.json); empty = <DSH_HOME>/.dsh/state.
gatekeeper.*see codeOptional LLM triage: provider / model / effort, fast paths, passcode, receipt & urge rules.

allow.json (inside dir) takes precedence over allowTargets, is re-read within 2 s, and is fail-closed: a corrupt file rejects everything instead of silently allowing all.


Safety notes / 安全边界

  • Writes are opt-in: with writeEnabled: false the plugin changes nothing about any session.
  • No shell, no arbitrary code: the mailbox only reads/writes JSON files under dir; sessions run with their own permissions. Letter text is passed as data (never as instructions) and the gate model gets no tools.
  • Prompt-injection hardening: gate calls are locked to one provider/model/effort and carry no tool list.
  • Fail-closed allow-list, optional shared secret, and a passcode gate for shared directories.
  • The panel is read-mostly: destructive actions are soft-delete or explicit confirmations.

Requirements / 环境

  • DSH >=0.2.1-0 <0.3.0 (desktop host with the agents / sessions / tools / llm services).
  • Node.js >=22 for the bundled tools; Windows, macOS and Linux are supported (paths are resolved from the environment, never hard-coded).

Development / 开发

npm run bundle      # build the client half (tsdown + scope wrapper)
npm run smoke       # core smoke suite
npm run smoke-ui    # panel / wakeability suite
npm run smoke-ops   # operations + archive suite
npm run smoke-gate  # gatekeeper suite

The host half (lib/index.js) is hand-written ESM and is not generated; the build only compiles src/client/*.ts into lib/client.js.


License

MIT — see LICENSE.