DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

sailoflight /

sailoflight/dsh-tui-compact-carry

Verified

TUI-only DeepSeek Harness plugin: compaction count/progress in the status line + /carry to bring a cached compaction summary into the current session

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@46600994

dsh-tui-compact-carry

A TUI-only plugin for DeepSeek Harness (dsh): compaction count + live progress in the status line, and /carry to bring a cached compaction summary into the current session.

⚠️ 只支持 TUI(@deepseek-harness-tui/dsh-tui)。状态行依赖 ctx.tuiStatus, Web / headless 没有这个服务,装上去不会报错但也没有状态行。命令本身在两端都能跑, 但本插件刻意只面向 TUI 场景。

一个零依赖的小插件(只用 Node 内置模块),解决 TUI 里压缩体验的三个痛点:

  1. 看不到压缩在跑 —— 自动压缩在 TUI 主界面完全无声,十几秒里没有任何指示。
  2. 数不清压过几次 —— 一屏之外的历史压缩次数没有地方可看。
  3. /new 之后上下文归零 —— 刚压缩出来的完整摘要带不到新会话里。

它做什么

能力 实现
状态行实时显示已压缩次数(+ 最近一次回收 token) ctx.on('session/event') 数 compaction/summary → ctx.tuiStatus.set('compact-carry', …)
压缩进行中显示「压缩中…(第 N 次 · Ns)」 compaction/start 进进度态、summary/end 退出;1 秒一跳,progressMaxMs 兜底
手动 /compact 后自动缓存摘要 认签名 sourceCommandId === undefined && turn == null(出回合 + 非命令驱动)→ compaction/end 时置 pending,状态行出现「· 摘要待带出」,弹 toast 提示敲 /carry
/carry:把缓存的摘要写进当前会话 invocation.agent.session → append('user/message', …, {surfaceOp:'append'}) → ctx.sessions.flush();带官方 checkpoint 标记 → TUI 渲染成折叠的「压缩检查点」行
/carry show:先看要带什么 打印来源会话、回收量、正文前 400 字
/carry all / merged / last all = 每一次压缩记录按序拼接;默认只带最后一条(官方已把上一份 checkpoint 合并进新摘要 = 全量)
/carry now:另建一个独立会话 ctx.sessions.create('carry-<uuid>') → append → flush;之后 /resume 选第 1 条
/carry off:清掉待带出摘要 清 pending(不删已有的会话记录)
/compactstats:看账目 次数 + 累计回收 + 待带出/已带出状态
关键节点弹一条 toast ctx.get('tuiToast', false)(拿不到该服务就静默跳过)

为什么要进度行:自动压缩在 TUI 主界面是无声的——channel.compact() 里的「正在压缩会话…」 只属于手动 /compact;全包响应 compaction/start|end 的只有轨迹页投影 (trajectory/projection.js)。所以插件把这两个事件搬到了状态行上。

安装

要求:Node.js ^22.19.0 || >=24.0.0,dsh 已在 PATH 上。

方式 A:按 bundle 安装(推荐)

dsh plugin --profile dsh-tui add github:sailoflight/dsh-tui-compact-carry

package.json 里声明了 dsh.bundle,所以 dsh plugin add 会把它作为一层配置装进 profile。 装完重启该 profile 才生效(manifest/包元数据不热更新;只有用户自己的 cordis.patch.yml 才热重载)。

方式 B:绝对路径挂载(零依赖,不需要 pnpm)

本插件零外部依赖,所以也可以直接把 index.js 放到插件目录,再用绝对路径挂一行:

mkdir -p "$HOME/.dsh/plugins/dsh-tui-compact-carry"
cp index.js "$HOME/.dsh/plugins/dsh-tui-compact-carry/"

在 $DSH_HOME/profiles/dsh-tui/cordis.patch.yml(默认 $DSH_HOME = ~/.dsh)里加:

- insert:
    - id: dsh-tui-compact-carry
      name: /home/<you>/.dsh/plugins/dsh-tui-compact-carry/index.js
      config: {}

⚠️ 方式 A 与方式 B 二选一,别同时用(同一个 id 挂两次会报 duplicate loader entry id)。

用法(三步)

/compact        # ① 官方手动压缩一次(拿到含最近几轮的最新全量摘要)
/new            # ② 起新会话 —— dsh-tui 原生行为,插件不参与
/carry          # ③ 在**新会话里**敲:把刚缓存的摘要写进本会话(首条是折叠的「压缩检查点」)
  • ②③ 之间的顺序不重要,重要的是 ③ 要在你希望摘要落在的那个会话里敲——插件写的是 "当前 agent 的会话",不猜、不追、不抢。
  • 想先看内容:/carry show;想带全部压缩记录:/carry all;改主意:/carry off。
  • 摘要缓存默认 30 分钟内有效(autoArmWindowMs),过期就提示"过期"并要求重新 /compact。
  • 没有摘要可带时 /carry 直接报错,让你先按官方 /compact(不猜测、不虚报、绝不自己动手压)。
  • 摘要缓存是进程内的:/compact 之后没敲 /carry 就重启了 TUI,缓存就没了,重新压一次即可。

设计约束(三条硬规则)

一、不订阅任何 TUI 决策事件

tui/session-switched、tui/input、tui/compact 等属于 TUI 决策事件 (TUI_DECISION_EVENT_NAMES)。TUI 装了全局钩子 ctx.on('internal/listener', …) (dsh-adapter/decision-guard.js 的 installDecisionGuard),cordis 对每一次订阅都会触发它。 插件用裸 ctx.on 注册决策事件时:

  • 若插件没有 Component 身份 → 记 warn 并返回 () => false(空 disposer),监听器根本没注册;
  • 若有身份但没有 tui.dsh/v1alpha1#DecisionEvents 契约 → registerDecisionHandler 抛错, 同样被捕获、告警、拒绝。

dsh-adapter/extension-events.js 也写得很明白:"Handlers come from the host-mediated DecisionEvents registry. Raw Cordis ctx.on listeners are intentionally absent from this path."

⇒ 普通插件订阅决策事件是死代码。要用决策事件,得把一个完整 TUI 扩展组件 (manifest + DecisionEvents 契约 + 权限声明 + 授权文件)装进 dsh-tui 自己的扩展宿主里—— 那是另一量级的工程,而且正好是"插件深度介入 /new 切换"的高风险区。 本插件的选择是退出这条路径:计数/进度/缓存全部走 session/event (普通事件,决策守卫不管)。

排查提示:如果你看到"状态行更新了、但新会话里什么都没发生",八成就是踩了这个区别—— 普通事件能用,决策事件被静默拒绝。

二、插件绝不自己触发压缩

TUI 把「手动压缩」做成了一个必须在会话切换前结算的事务 (dsh-adapter/channel/compaction.js:"Owns the one manual compaction transaction that must settle before a switch."),settleCompaction 被注入 newSessionAction / fork / rewind / session-tree。

插件绕过这个事务直接触发压缩 → /new 的 settle 无事可等 → 切换与压缩赛跑,会卡在半路 (表现为:压缩完成后输入栏不再接受输入、盲打 /new 后卡在选择界面、方向键以 [[A 之类的原始序列堆进输入行)。

⇒ TUI 没有给插件留"安全触发官方压缩"的入口(tui/compact 是给插件用的否决钩子, 不是触发器)。所以:压缩只由用户按官方 /compact,插件只负责"取现成的摘要 + 搬运"。 离线冒烟的最后一道门就是 engineCalls.length === 0。

三、完全不参与 /new 的切换事务

插件连"新会话出现了"这件事都不需要知道:/carry 是在新会话里敲的,写的是 invocation.agent.session(就是"当前会话"),不依赖任何切换通知。 /new 的建会话/清屏流程逐字保持 dsh-tui 原样。

有一个默认关闭的可选自动版(autoCarryOnNew):新会话出现后延迟 autoCarryDelayMs 自动写入。它走的是普通事件 session/created,刻意绕开 /new 的切换事务; 那条监听器抛错会让 attach 回滚,所以全程 try/catch,里面只做"读字段 + 调度定时器"。 默认选择仍是手动 /carry。

与宿主版本的耦合:session format V4(0.6.0)

DSH 的会话格式升到 V4 之后,/carry 会报 format v4 message requires a producer-owned source kind。原因是 0.5.0 写的是 V3 时代的 泛型包装 source: { kind: 'plugin', plugin: 'compact' };V4 的准入校验直接拒绝 kind: 'plugin',要求消息源报出「生产者自己的 kind」:

形状 结果
{ kind: 'plugin', plugin: 'compact' } ❌ 抛错
{ kind: 'compact-checkpoint', compactionId } ✅ 通过(官方 checkpoint 标记)
{ kind: 'plugin:dsh-tui-compact-carry', compactionId } ✅ 通过(第三方插件的生产者 kind 约定)

判据在 dsh-session-format-v3-to-v4/lib/index.js 的 assertV4SourceRowAdmission / assertV4MessageSources,编码与解码两条路径都调用——所以既是写不进去、也是读不出来。 compact-checkpoint 与 dsh-compaction 的 COMPACT_CHECKPOINT_MARKER 逐字一致; TUI/客户端用 isCompactCheckpointSource 只比对它。压缩不变量只作用于「替换型 surface 事件」, 而本插件用 surfaceOp: 'append',不受 compactionId 配对约束。

同一版还删掉了 Session.events(只剩 snapshotEvents() / ownEvents()), 所以 0.5.0 里遍历 session.events 的兜底读取会 TypeError;0.6.0 改用 snapshotEvents()。

提示:这类耦合会随宿主升级漂移。升级 dsh / dsh-tui 之后先跑一遍离线冒烟。

离线冒烟

node smoke.mock.mjs      # 或 npm test

60 项硬断言,失败则 exit≠0,不需要装进任何 profile(用假 ctx)。四道核心回归门:

  • 引擎门:假压缩引擎当陷阱,整场跑完断言 engineCalls.length === 0(插件一次都没碰引擎)。
  • 决策事件门:断言 listeners 里没有 tui/session-switched。
  • V4 生产者 kind 门:本地镜像规则扫过每一条注入消息(kind 非空、≠ plugin、无 plugin 字段、compactionId 非空);若本机装了 @deepseek-ai/dsh,还会加载宿主真实的 assertV4RowAdmission 复核同样这批消息,并做一次「旧形状必须被拒」的反向对照(防止假绿)。
  • 会话快照门:假 session 不暴露 events,并构造一条"只存在于会话日志里、插件从没观察到" 的压缩记录,验证 /compactstats、/carry show 能从 snapshotEvents() 补出历史。

其余覆盖:状态行的计数/进度/待带出/已带出流转、/new 纯净性(没有任何写入动作)、 不重复注入、/carry all|show|off|now、非法参数、carry-<uuid> 的全局唯一 id、 可选自动版(fork/子会话不注入、延迟调度、监听器绝不抛错)。

已知限制

# 限制 说明
1 /carry now 建的新会话不 attach 到 TUI 的 workspace registry(只写了 meta.cwd) /resume 读的是持久化后端的全量会话,同 cwd 归到同一项目 → 仍会出现在列表里
2 伪造的 compactionId(carry-<uuid>)没有对应的 compaction/summary 事件 纯显示层影响;把 useCheckpointMarker 设为 false 可完全规避(代价:不折叠成「压缩检查点」行)
3 摘要缓存只覆盖本进程 有 live 会话回读兜底;重启 TUI 后对已 dispose 的旧会话取不到记录 → 就不带(保守)
4 注入行是否"立刻"渲染未逐一验证(写入是确定的:flush 完就在持久化里) 状态行「已带 N 条摘要」是立刻可见的确认;万一首条没马上显示,/resume 重进一次即可
5 压缩不由插件触发 → 要自己按 /compact,且多了一条 /carry TUI 侧硬约束(见「设计约束 二」)
6 自动缓存靠事件签名判定(sourceCommandId === undefined && turn == null) 若将来 payload 形状变了,降级是安全的:不会误带,只是要手动 /compact 后重压
7 贴住了两个会随宿主升级漂移的契约(消息源 kind、Session 快照读法) 都写进了离线冒烟;升级后先 npm test

配置项(index.js 顶部 CONFIG)

键 默认 含义
carryMode 'merged' 命令参数可覆盖;'merged'=最后一条全量,'all'=每一次压缩按序
maxCarryChars 0 注入正文字符上限,0=不截断
maxKeepSessions / maxItemsPerSession 8 / 64 内存缓存上限
statusKey / showStatus 'compact-carry' / true 状态行 key 与开关
showProgress / progressElapsed true / true 压缩进行中的进度态;是否带秒数
progressMaxMs 900000(15 min) 进度兜底上限:超过视为 compaction/end 丢失,回到计数行
useCheckpointMarker true 用官方 kind:'compact-checkpoint' 标记(渲染成原生压缩检查点行)。设为 false 则改用 plugin:dsh-tui-compact-carry(代价:不折叠成检查点行)
showNextStepToast true 关键节点弹一条 tuiToast(拿不到该服务则静默跳过)
autoArmOnManualCompact true 用户按官方 /compact 后自动缓存摘要(等 /carry 带入)
autoArmWindowMs 1800000(30 min) 缓存的有效窗口;超过就不带。0/负数 = 不过期
autoCarryOnNew false 自动版:新会话出现后自动写入。默认关
autoCarryDelayMs 1500 自动版的延迟:等 /new 的切换事务落定后再写

许可

MIT

—/ 5

No ratings yet

Verified DSH bundle

Commit 466009949ff6

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout