@falling-ts/dsh-force-compact 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 the dsh-force-compact contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.cn.md ADDED
@@ -0,0 +1,170 @@
1
+ # dsh-force-compact
2
+
3
+ [English](README.md) | 中文
4
+
5
+ `@falling-ts/dsh-force-compact` 是一个 DSH **Cordis 函数插件**,**钩住官方的核心
6
+ 模型请求**。每次请求模型前都会读取"强制压缩配置"(`falling-ts-force-compact`)设置:
7
+
8
+ - 当 `disableThinking` 开启时,在**请求参数中强制关闭思考/推理**;
9
+ - 当会话上下文总 tokens 数**达到** `autoThresholdTokens` 时,**不请求模型**,
10
+ 而是**强制执行强制压缩**。
11
+
12
+ 此外,插件还在每次会话持久化检查点(`session/flush`)压缩有用历史:拥有自己的
13
+ 区间选择策略与 LLM 摘要器,并把持久的 surface 变更委托给 `compaction` 服务。
14
+
15
+ ## 工作原理
16
+
17
+ 插件钩住官方的模型请求 Waterfall,使判断发生在**每次模型请求之前**(即"钩住
18
+ 核心模型请求"的需求),并保留持久化检查点:
19
+
20
+ - **`agent/request`** —— 围绕冻结调用配置的 Waterfall。当 `disableThinking` 开启时,
21
+ 返回的 `LlmCallConfig` 携带 `reasoningEffort: 'off'`,LLM 适配器将其映射为
22
+ `thinking: { type: 'disabled' }`,即进程内**每次模型请求**都关闭思考。设置在此
23
+ (每次请求)读取,因此 `settings.yaml` 的改动在下一次请求即生效。
24
+ - **`agent/pre-step`** —— 每个模型步骤之前的 Waterfall。通过 `tokenMeter` 服务
25
+ 读取会话**上下文总 tokens 数**;当其**达到或超过** `autoThresholdTokens` 时,
26
+ 返回 `{ kind: 'reject' }` **不发起模型请求**,并通过 compaction 服务的
27
+ `compactRegion`(经 `ctx.get('compaction')` 实时读取)**保留最新的
28
+ `retainLatestTokens` 个 token 逐字不变**,并将其余**头段**一次性浓缩为一个摘要节点,
29
+ 让循环以更小的上下文重试。
30
+ - **`session/flush`** —— 一个被等待(awaited)的 `parallel` 持久化检查点。检查点
31
+ 会等待所有监听器完成,因此压缩在调用方继续之前就已结束,摘要保证落盘。
32
+ - **`/force-compact`** —— 通过 `/` 选择执行的斜杠命令,强制压缩该 Agent 的会话
33
+ 上下文。其 handler **不发送模型请求**:Agent **空闲**时经 `compactNow`
34
+ (owner `null`,空闲手动入口,引擎自身区间选择)立即压缩;**繁忙**时
35
+ `compactNow` 被拒绝,handler **插入一个 process-local 强制标记**(JS 内存记录,
36
+ 无持久态、无 timer),由 `agent/pre-step` 钩子在下一个模型步骤读取。
37
+ 读到强制标记时,该步骤**跳过 token 阈值门禁**,按 `retainLatestTokens`
38
+ 语义选区(保留最新 N 个 token、头段一次性压缩)并经 `compactRegion`(current-turn owner,可在 mid-turn 执行)执行,并返回 `{ kind: 'reject' }`
39
+
40
+ - **`agent/status`** —— agent 生命周期迁移监听器。当 agent 转入 `idle`
41
+ (所有轮次结束,含子代理,下一次人为对话之前)且 `turnEndForceCompactionEnabled`
42
+ 为 `true` 时,经 `compactNow`(owner `null`,空闲手动入口)压缩会话——使用
43
+ 引擎自身的区间选择(空闲路径无法选择自定义 token 比例,故无一轮结束比例参数)。
44
+
45
+ 支撑模块:
46
+
47
+ - **`src/hooks/guard.js`** —— 每次请求的门禁:`agent/request` 关闭思考 +
48
+ `agent/pre-step` 阈值门禁 + 强制压缩 + `/force-compact` 的 process-local 强制标记。
49
+ - **`src/hooks/command.js`** —— `/force-compact` 斜杠命令:Agent 空闲时经 `compactNow`
50
+ 压缩;繁忙时插入强制标记,待下一个模型步骤消费。
51
+ - **`src/hooks/idle.js`** —— 一轮结束强制压缩:`agent/status` 上的 `idle` 监听器,
52
+ 经 `compactNow`(引擎自身区间选择)压缩。
53
+ - **`src/engine/region.js`** —— 插件自己的 head-anchored 区间选择:`selectRegion`(检查点
54
+ 路径)与 `selectEarliestByTokens`(供 `agent/pre-step` 使用):前者按 surface
55
+ 节点数保留最近尾段,且都把区间末端对齐到 `user/message` 边界(始终是一个平衡
56
+ 边界)。`idle` / `/force-compact` 路径改用 `compactNow` 的引擎自身区间选择。
57
+ - **`src/engine/summarizer.js`** —— 插件自己的一次性 LLM 摘要器:回放区间消息,把压缩
58
+ 指令作为最后一条 user 消息追加,通过 `ctx.llm` 流式生成,返回浓缩后的检查点。
59
+ - **`src/engine/checkpoint.js`** —— 检查点编排器:选区间 → 投影区间消息 → 运行预览 + 收缩
60
+ 门禁 → 把持久变更委托给 compaction 服务的 **`compactRegion(start, end,
61
+ agent, signal)`**(经 `ctx.get('compaction')` 实时读取;权威摘要器)。
62
+
63
+ ```
64
+ agent/request(payload, next) # 每次模型请求
65
+ settings.get("falling-ts-force-compact") -> disableThinking?
66
+ return { ...config, reasoningEffort: "off" } # 关闭思考
67
+
68
+ agent/pre-step(payload, next) # 每个模型步骤之前
69
+ tokenMeter.measure(session).totalTokens >= autoThresholdTokens?
70
+ 否 -> next() # 让模型请求继续
71
+ 是 -> compactRegion(head-before-retainLatestTokens, signal) # 强制压缩
72
+ return { kind: "reject" } # 本次步骤不请求模型
73
+
74
+ agent/status({ agent, status }) # agent 生命周期迁移
75
+ status === "idle" && turnEndForceCompactionEnabled?
76
+ 是 -> compactNow(agent, freshSignal) # 一轮结束压缩(空闲)
77
+ 否 -> 跳过
78
+
79
+ session/flush(session) # 持久化检查点
80
+ agents.get(session.id) -> 实时 Agent(不存在则跳过)
81
+ region.selectRegion(session) -> {start, end} 或 null(跳过)
82
+ projectRegionMessages() -> 区间消息
83
+ summarizer.summarize() -> 预览 + 收缩门禁
84
+ compaction.compactRegion(start, end, agent, signal)
85
+ null -> 无操作(没有可压缩内容)
86
+ result -> 已将区间压缩为一个摘要节点
87
+ ```
88
+
89
+ ## 安装
90
+
91
+ 作为可安装 bundle(推荐):
92
+
93
+ ```sh
94
+ # 从 git:
95
+ dsh plugin --profile web add github:falling-ts/dsh-force-compact
96
+ # 从本地检出:
97
+ dsh plugin --profile web add ./dsh-force-compact
98
+ ```
99
+
100
+ 或从本地检出,以 `--patch` overlay 挂载(不安装):
101
+
102
+ ```sh
103
+ dsh web --patch dsh-force-compact/cordis.patch.yml
104
+ ```
105
+
106
+ 该层把 `force-compact` 函数插件插入当前组合(composition),不改动默认发布
107
+ 配置。
108
+
109
+ ## 设置(强制压缩配置)
110
+
111
+ 当 `settings` 服务已挂载(web bundle 通过 `@deepseek-ai/dsh-settings-file` 始终
112
+ 挂载它)时,插件会注册 `falling-ts-force-compact` 设置命名空间,使五个参数可从
113
+ `$DSH_HOME/settings.yaml` 配置(`falling-ts-` 前缀用于防止与其他插件的键冲突):
114
+
115
+ | 键 | 类型 | 默认值 | 作用 |
116
+ | --- | --- | --- | --- |
117
+ | `disableThinking` | `boolean` | `true` | 为 `true` 时,**每次模型请求**都携带 `reasoningEffort: 'off'`,LLM 适配器将其映射为 `thinking: { type: 'disabled' }`——即请求时关闭提供方的思考/推理。同样作用于插件自己的摘要调用。 |
118
+ | `autoThresholdTokens` | `number`(≥ 32000) | `32000` | 自动压缩触发阈值(单位 tokens)。**在请求模型前**,通过 `tokenMeter` 测量会话上下文总 tokens 数;当其**达到或超过**该值时,**不请求模型**,而是强制执行一次压缩。`session/flush` 检查点路径也把它作为触发门禁。**下限 32000**:低于 32000 的值读取时自动抬升至 32000。 |
119
+ | `retainLatestTokens` | positive int(≥ 8000) | `8000` | **保留最新上下文的绝对 token 数**——`agent/pre-step` 阈值门禁或 `/force-compact` 强制标记触发时,从会话**最新条目**起,按官方 `tokenMeter` 的逐节点计数**反向**累加 token,直到 ≥ 该值**停止**;该截点之前的**所有条目一次性**发往大模型做摘要(原条目被遮蔽/跳过),保留段逐字不变。**下限 8000**:低于 8000 的值读取时自动抬升至 8000。
120
+ | ~~`forceEarliestRatio`~~ | — | — | *已移除。* 强制标记路径现同样使用上方 `retainLatestTokens`(见上行),不再单独保留一个比例参数。 |
121
+ | `turnEndForceCompactionEnabled` | `boolean` | `true` | **是否开启一轮结束强制压缩**——为 `true` 时,agent 转入 `idle`(所有轮次结束,含子代理,下一次人为对话之前)时经 `compactNow`(引擎自身区间选择)强制执行一轮结束压缩。 |
122
+
123
+ `$DSH_HOME/settings.yaml` 示例:
124
+
125
+ ```yaml
126
+ falling-ts-force-compact:
127
+ disableThinking: true
128
+ autoThresholdTokens: 32000
129
+ retainLatestTokens: 8000
130
+ turnEndForceCompactionEnabled: true
131
+ ```
132
+
133
+ 当 `settings` 服务不存在时,插件回退到同样的默认值,压缩照常进行——设置命名空间
134
+ 是可选的,绝非硬依赖。
135
+
136
+ ## 行为说明
137
+
138
+ - **运行时依赖:** `compaction` 服务,由 preset 平面(`include:agent-presets:compaction-basic`,默认启用并挂载)提供,事件时经 `ctx.get('compaction')` 实时读取。不可用时插件不做任何事(强制压缩路径会降级为让请求继续)。
139
+ - **可选依赖:** `agents` 服务。仅供 `session/flush` 检查点路径使用;若某次
140
+ flush 触发时 `Agent` 已被注销,插件打印 `no live agent … — skipping` 并跳过
141
+ 该检查点。`agent/*` Waterfall 的 payload 直接携带 `Agent`,无需 `agents` 查找。
142
+ - **可选依赖:** `settings` 服务。不存在时,参数回退到默认值(`disableThinking:
143
+ true`、`autoThresholdTokens: 32000`、`retainLatestTokens: 8000`、
144
+ `turnEndForceCompactionEnabled: true`)。
145
+ - **可选依赖:** `tokenMeter` 服务。供 `agent/pre-step` 阈值门禁使用;不存在时,
146
+ 门禁回退到对会话 surface 内容的粗略字符估算。
147
+ - **每次请求读取设置:** 两个参数都**每次模型请求**读取
148
+ (同步 `settings.get('falling-ts-force-compact')`),因此 `settings.yaml` 的改动在下一次
149
+ 请求即生效,无需重启。
150
+ - **信号(signal):** `agent/*` Waterfall 转发当前 turn 的 signal;
151
+ `session/flush` 检查点与 `agent/status` idle 监听器各新建一个 `AbortController`。
152
+
153
+ ## 已知限制
154
+
155
+ - 插件在**持久化检查点**(`session/flush`)时压缩,此时 `Agent` 可能尚未被
156
+ 注销。如果你的部署在最后一次 flush 之前就注销了 `Agent`,最后一次压缩可能被
157
+ 跳过;若该时序对你重要,可改为监听 `agent/disposed`(其 payload 直接携带
158
+ `Agent`)。
159
+ - 插件自己的摘要器是**预提交预览 + 收缩门禁**;持久摘要内容由 `compaction`
160
+ 服务权威生成。
161
+ - 强制压缩门禁在达到阈值时**拒绝所提议的模型步骤**,随后依赖循环以更小的
162
+ 上下文重试。若 `compactRegion` 找不到安全区间(例如已无可压缩的有用内容),
163
+ 则让请求按原样继续,而非循环。
164
+ - `idle` 与 `/force-compact` 路径使用 `compactNow`(引擎的空闲手动入口),其
165
+ 区间选择是引擎自身的(基于 `retainTokens`),而非插件可调比例。插件可调比例
166
+ (`retainLatestTokens` 驱动 `agent/pre-step` 钩子
167
+ (current-turn owner `compactRegion`)遵守。
168
+ - 不注册任何 client/browser UI;插件是纯 Host 插件。参数可通过
169
+ `falling-ts-force-compact` 设置命名空间调参(未来某个动态 client 插件可读取它
170
+ 来提供设置页面),并可通过 `[force-compact]` 日志行与持久日志观察。
package/README.md ADDED
@@ -0,0 +1,479 @@
1
+ # dsh-force-compact
2
+
3
+ **Local-first, aggressive context compaction for DeepSeek Harness agents.**
4
+
5
+ A DSH **Cordis function plugin** that keeps your agent's working context lean *by design*, so you
6
+ can serve a **genuinely large effective window** against a **self-hosted llama.cpp running
7
+ Qwen3.8‑27B** at modest context — getting smooth, low‑latency, high‑quality answers without API
8
+ costs or any data leaving your machine.
9
+
10
+ English | [中文](README.cn.md)
11
+
12
+ ---
13
+
14
+ ## Why run Qwen3.8‑27B locally on llama.cpp, at low context?
15
+
16
+ Most harness setups bolt a big frontier model onto a short context budget. This plugin makes the
17
+ opposite bet: **you own the weights, the endpoint, and the context budget.**
18
+
19
+ - **Self‑hosted inference.** Point the agent at a local OpenAI‑compatible llama.cpp server
20
+ running `Qwen3.8‑27B` (GGUF / NVFP4 / MTP variants all work through the standard DeepSeek
21
+ adapter path — no separate llama.cpp adapter required). Conversations never leave the box.
22
+ - **Stay fast by staying low.** llama.cpp serves a 27B model with a modest context while keeping
23
+ per‑step latency and VRAM in check. Aggressive compaction is what makes that viable: rather than
24
+ fight a small hard cap, the plugin **shrinks the conversation itself**, so the agent always
25
+ reasons over a tight, high‑signal prompt while effectively reaching a far larger working
26
+ memory.
27
+ - **Thinking‑off by default.** `disableThinking: true` turns off the model's internal reasoning
28
+ effort on **every** outbound call (business requests *and* the plugin's own summarization
29
+ calls) — faster loops, less token burn. Enforced twice for reliability (see
30
+ "Backend‑agnostic thinking control").
31
+ - **Cheaper, private, yours.** No per‑token billing, no data egress, and you dial the exact
32
+ model/context tradeoff.
33
+
34
+ > **Net effect:** a big‑window *experience* (long sessions, many tools, multi‑turn goals)
35
+ > delivered by a locally‑served 27B model at low context. The compression is what makes it feel
36
+ > effortless — dramatically better compression efficiency means a dramatically smoother agent
37
+ > experience.
38
+
39
+ ---
40
+
41
+ ## What the plugin does
42
+
43
+ Two compaction engines coexist behind one facade (`resolveCompaction`), transparent to callers:
44
+
45
+ | Engine | When used | Notes |
46
+ |--------|-----------|-------|
47
+ | **Official** | When the `compaction` service is reachable in the agent realm | Preferred; delegates to `compaction/basic`. |
48
+ | **Builtin** | Automatic fallback when the service is realm‑isolated (typical standard preset) | Self‑contained persistent transaction using only `ctx.sessions` / `ctx.llm.stream` / `ctx.tokenMeter`. Reuses the official `compaction/*` event vocabulary, so it survives cross‑build replay with no `ignorable` hacks. |
49
+
50
+ You never toggle between them — official wins when reachable, builtin takes over otherwise.
51
+
52
+ ### Trigger points
53
+
54
+ - **Per‑request guard (`agent/pre-step`)** — reads the session's *projected* context tokens (the
55
+ exact number the harness renders bottom‑right, provider‑anchored). When it reaches
56
+ `autoThresholdTokens`, it rejects the outgoing model request and compacts the head instead,
57
+ retaining the latest `retainLatestTokens` verbatim.
58
+ - **Turn‑end / idle compaction (`agent/status` → `idle`)** — when the agent quiesces (all turns
59
+ and sub‑agents done), optionally compacts through `compactNow` (gate:
60
+ `turnEndForceCompactionEnabled`).
61
+ - **Manual `/force-compact` slash command** — acts on a *busy* or *idle* agent: compacts
62
+ immediately when idle, or queues a process‑local force flag consumed at the next model step
63
+ when busy.
64
+ - **`session/flush` checkpoint** — the awaited durability checkpoint.
65
+
66
+ Every path funnels into the single *"compaction result landed in the session"* boundary — the
67
+ same place the live UI signal is emitted (below).
68
+
69
+ ### Decision basis is *provider‑anchored*
70
+
71
+ Decisions key off `projectedTokens` — the same figure shown in the UI corner — so the plugin
72
+ never drifts from what you see. Heavy CJK / tool‑JSON content is priced at the meter's
73
+ chars‑per‑token density for consistency, and a threshold‑aware shrink gate skips summarization
74
+ LLM calls that provably could not pull the session below the threshold (eliminating the
75
+ low‑threshold dead loop).
76
+
77
+ ### Shadow‑price accounting aligned with the meter
78
+
79
+ The builtin transaction bills `shadowedTokenCount` from the **same** `tokenMeter.measure`
80
+ per‑node prices the official engine uses, so the meter's collapse protocol settles the drop
81
+ correctly — the bottom‑right counter goes *down* after compaction instead of drifting upward.
82
+
83
+ ### Backend‑agnostic thinking control
84
+
85
+ `disableThinking` is enforced at **two complementary seams**:
86
+
87
+ 1. **Request seam** — `reasoningEffort:'off'` → the DeepSeek adapter serializes
88
+ `thinking:{type:'disabled'}` (real DeepSeek APIs honor it).
89
+ 2. **Wire seam (`llm/stream`)** — the plugin appends top‑level `reasoning_effort:"none"`
90
+ post‑serialization, which llama.cpp's OpenAI‑compatible layer parses natively
91
+ (`server‑common.cpp` maps it to `enable_thinking=false` regardless of template capability).
92
+ Real DeepSeek endpoints simply ignore the unknown key.
93
+
94
+ Result: thinking is genuinely off on **any** backend — including your local llama.cpp — with no
95
+ target‑sniffing heuristic to miss a route.
96
+
97
+ ### Live UI status
98
+
99
+ A tiny host→client messenger (a `liveUi` settings field, mirrored live to the browser) paints a
100
+ badge beside the turn:
101
+
102
+ - 🟥 `compressing` — pinned red `[强制压缩中>>>]`, fired just before a compaction commits;
103
+ - 🟢 `done` — pinned green `[压缩完成!]`, fired the instant a compaction result lands in the
104
+ session, then falls back to a fresh random "working" pair after 3 s;
105
+ - 🔵 `working` — otherwise a playful random one‑liner ("正在缝合上下文…", "正在憋大招…").
106
+
107
+ Publishers are fail‑safe: a messenger glitch can never disturb the actual compaction.
108
+
109
+ ---
110
+
111
+ ## How it works
112
+
113
+ The plugin hooks the official model‑request Waterfalls so the decision happens **right before a
114
+ model request is made**, plus the durability checkpoint:
115
+
116
+ - **`agent/request`** — a Waterfall around the frozen call configuration. When `disableThinking`
117
+ is on, the returned config carries `reasoningEffort:'off'`. Settings are read **per request**,
118
+ so a `settings.yaml` edit is picked up on the next request.
119
+ - **`agent/pre-step`** — a Waterfall before each model step. Reads the session's *projected*
120
+ tokens; when `>= autoThresholdTokens` it returns `{ kind:'reject' }` (no model request) and
121
+ compacts the head while retaining the latest `retainLatestTokens`.
122
+ - **`session/flush`** — an awaited `parallel` checkpoint, so compaction completes before the
123
+ caller proceeds.
124
+ - **`/force-compact`** — a slash command acting without sending the line to the model:
125
+ immediate `compactNow` when idle; queued force flag when busy.
126
+
127
+ ```
128
+ agent/request(payload, next) # every model request
129
+ disableThinking? -> { ...config, reasoningEffort: "off" }
130
+
131
+ agent/pre-step(payload, next) # before each model step
132
+ projectedTokens >= autoThresholdTokens?
133
+ no -> next() # let the model request proceed
134
+ yes -> compactRegion(head-before-retainLatestTokens, signal)
135
+ return { kind: "reject" } # NO model request this step
136
+
137
+ agent/status({ agent, status }) # lifecycle transition
138
+ status === "idle" && turnEndForceCompactionEnabled?
139
+ -> compactNow(agent, freshSignal) # turn-end compaction
140
+
141
+ session/flush(session) # durability checkpoint
142
+ select region -> project messages -> preview + shrink gate
143
+ -> compaction.compactRegion(start, end, agent, signal)
144
+ ```
145
+
146
+ Supporting modules:
147
+
148
+ - `src/hooks/guard.js` — per‑request guard: thinking‑off + threshold gate + forced flag.
149
+ - `src/hooks/command.js` — the `/force-compact` command.
150
+ - `src/hooks/idle.js` — turn‑end forced compaction.
151
+ - `src/hooks/wire-rewrite.js` — the `llm/stream` wire patch appending `reasoning_effort:"none"`.
152
+ - `src/engine/region.js` — head/tail‑anchored region selection (+ official pairing ledger).
153
+ - `src/engine/summarizer.js` — the one‑shot LLM summarizer.
154
+ - `src/engine/builtin.js` — the builtin persistent transaction (official `compaction/*` vocab).
155
+ - `src/core/projected.js` — the provider‑anchored `projectedTokens` reading.
156
+ - `src/core/ui-signal.js` — the live UI messenger.
157
+
158
+ ---
159
+
160
+ ## Install & verify
161
+
162
+ As an installable bundle (recommended):
163
+
164
+ ```sh
165
+ # from git:
166
+ dsh plugin --profile web add github:falling-ts/dsh-force-compact
167
+ # from a local checkout:
168
+ dsh plugin --profile web add ./dsh-force-compact
169
+ ```
170
+
171
+ or, from a local checkout, as a `--patch` overlay without installing:
172
+
173
+ ```sh
174
+ dsh web --patch dsh-force-compact/cordis.patch.yml
175
+ ```
176
+
177
+ Plugin loaded ⟺ `~/.dsh/logs/dsh-force-compact.log` gains:
178
+
179
+ ```
180
+ [force-compact] debug logging enabled — writing [force-compact] lines to <absolute path>
181
+ ```
182
+
183
+ Verify a compaction happened:
184
+
185
+ ```
186
+ idle compaction (builtin) shadowed N nodes (~M tokens)
187
+ builtin compaction OK — replaced span seq[A..B] (N nodes, ~K tokens) with a P-char checkpoint
188
+ ```
189
+
190
+ ---
191
+
192
+ ## Settings (`$DSH_HOME/settings.yaml`, namespace `falling-ts-force-compact`)
193
+
194
+ | key | type | default | meaning |
195
+ |-----|------|---------|---------|
196
+ | `disableThinking` | boolean | `true` | Disable model reasoning effort on **every** outbound call (both seams above). |
197
+ | `autoThresholdTokens` | number ≥ 32000 | `32000` | Projected‑token trigger for the per‑request gate. Lower ⇒ more aggressive, leaner context. **Floor 32000** (stored values clamp back up at read time). |
198
+ | `retainLatestTokens` | positive int ≥ 8000 | `8000` | Retain the latest N tokens verbatim; send everything older to the summarizer in one batch. **Floor 8000**. Drives both the auto gate and the `/force-compact` path. |
199
+ | `turnEndForceCompactionEnabled` | boolean | `true` | Compact on the agent's `idle` transition. |
200
+ | `debug` | boolean | `true` | Emit `[force-compact]` diagnostics to the plugin log. |
201
+ | `logFile` | string | `~/.dsh/logs/dsh-force-compact.log` | Diagnostics destination (`~` expands to home dir). |
202
+ | `compactionMode` | `'realm' \| 'global'` | `'realm'` | Official‑service resolution strategy (priority‑1 path). |
203
+ | `builtinEnabled` | boolean | `true` | Gate for the builtin engine fallback. |
204
+ | `maxSummaryTokens` | integer (1024–200000) | `1024` | Cap on the summarizer LLM `maxTokens`. |
205
+
206
+ Example — an aggressive **local** profile:
207
+
208
+ ```yaml
209
+ falling-ts-force-compact:
210
+ disableThinking: true
211
+ autoThresholdTokens: 40000 # compact sooner ⇒ keep the live prompt small
212
+ retainLatestTokens: 8000
213
+ turnEndForceCompactionEnabled: true
214
+ ```
215
+
216
+ When the `settings` service is absent, the plugin falls back to the same defaults and still
217
+ compacts — the namespace is optional, never a hard dependency.
218
+
219
+ ### Tuning for low‑context llama.cpp
220
+
221
+ Serve Qwen3.8‑27B with a comfortable‑but‑modest context, then let the plugin decide the
222
+ effective window: keep `autoThresholdTokens` comfortably **below** your served context so the
223
+ live prompt stays small and latency flat, while the agent retains deep memory through the
224
+ compressed head. Because pressure is measured in *projected* tokens (provider‑anchored), the
225
+ threshold maps predictably onto what the UI shows you.
226
+
227
+ ---
228
+
229
+ ## Behavior notes & limitations
230
+
231
+ - **Runtime dependency:** the `compaction` service (preset plane
232
+ `agent-presets:compaction-basic`). Read live via `ctx.get('compaction')`; when unavailable the
233
+ forced‑compaction path falls through and lets the request proceed.
234
+ - **Optional dependencies:** `settings`, `tokenMeter`, `commands`, `llm`, `agents` are read via
235
+ `ctx.get(...)` with guards — a missing one degrades gracefully rather than crashing.
236
+ - **Per‑request settings read:** parameters are read per model request, so edits take effect on
237
+ the next request without a restart.
238
+ - **Signals:** the `agent/*` Waterfalls forward the current turn's signal; the `session/flush`
239
+ checkpoint and the `agent/status` idle listener each mint a fresh `AbortController`.
240
+ - **Persistence:** the durable output is the compaction bracket events + a `surfaceOp:replace`
241
+ `user/message` checkpoint, replay‑safe across builds.
242
+ - **Client half:** `web/client.js` adds a Settings section "强制压缩 / Force Compact" for editing
243
+ these values live (uSES‑safe mirror, no timers/state).
244
+ - **No timers except one:** the single intentional timer is the 3 s `publishDone` fallback
245
+ (presentation‑only, documented deviation). Otherwise the plugin is pure listeners + a
246
+ process‑local `Map` force flag.
247
+
248
+ ---
249
+
250
+ ## License
251
+
252
+ MIT (see LICENSE).
253
+
254
+ ---
255
+ ---
256
+
257
+ # dsh-force-compact —— 面向本地推理的「本地优先 · 激进压缩」插件
258
+
259
+ **为 DeepSeek Harness agent 提供的上下文压缩能力:本地优先、极简上下文、最大化 agent 使用体验。**
260
+
261
+ 这是一个 DSH **Cordis 函数插件**:它让 agent 的工作上下文**始终保持在紧凑、高信号的区间**,从而
262
+ 让你能用**自托管 llama.cpp 服务上的 Qwen3.8‑27B**(低上下文配置)跑出**接近大窗口**的体验——更低
263
+ 延迟、更高可用、数据不出本机,且不产生任何 API 费用。
264
+
265
+ [English](README.md) | 中文
266
+
267
+ ---
268
+
269
+ ## 为什么要在 llama.cpp 上本地跑 Qwen3.8‑27B、并且刻意压低上下文?
270
+
271
+ 主流做法是把大模型塞进短上下文预算里硬扛。本插件反其道而行:**权重、端点、上下文预算都由你自己
272
+ 掌控。**
273
+
274
+ - **自托管推理。** 把 agent 指向一个本地 OpenAI 兼容的 llama.cpp 服务器,运行 `Qwen3.8‑27B`
275
+ (GGUF / NVFP4 / MTP 变体均可走标准 DeepSeek 适配器路径,**无需单独的 llama.cpp 适配器**)。
276
+ 对话全程不离开本机。
277
+ - **低上下文也能又快又省。** llama.cpp 允许你用适中上下文服务 27B 模型,保持单步延迟与显存都可控。
278
+ 激进压缩正是让它可行的关键:不与小硬上限较劲,而是**直接收缩会话本身**——agent 永远在一个紧凑、
279
+ 高信号的小 prompt 上推理,却等效获得更大的工作记忆。
280
+ - **默认关闭思考。** `disableThinking: true` 对**每一次出站调用**(业务请求 + 摘要调用)都关闭模型
281
+ 的内部推理努力——循环更快、token 消耗更低,并在两个互补缝上双重保障(见下文)。
282
+ - **更省钱、更私有、归你。** 无按 token 计费、无数据外泄,模型与上下文的取舍完全由你调。
283
+
284
+ > **净效果:** **大窗口的体验**(长会话、大量工具调用、多轮目标)由一个本地服务的 27B 模型 +
285
+ > 低上下文交付。**压缩效率的大幅提升,直接换来 agent 使用体验的大幅改善**——这就是本插件的核心价值。
286
+
287
+ ---
288
+
289
+ ## 插件做了什么
290
+
291
+ 两条压缩引擎通过统一 facade(`resolveCompaction`)并存,对调用者透明:
292
+
293
+ | 引擎 | 何时使用 | 说明 |
294
+ |------|----------|------|
295
+ | **官方** | agent realm 内可解析到 `compaction` 服务时 | 首选,委托给 `compaction/basic`。 |
296
+ | **内置** | 官方服务被 realm 隔离时自动接管(典型标准预设) | 自包含持久事务,仅依赖 `ctx.sessions` / `ctx.llm.stream` / `ctx.tokenMeter`;复用官方 `compaction/*` 事件词汇,跨 build 重放存活、无需 `ignorable` hack。 |
297
+
298
+ 你**无需手动切换**:官方可达就用官方,不可达才落到内置。
299
+
300
+ ### 触发点
301
+
302
+ - **每请求门禁(`agent/pre-step`)** —— 读取会话的 *投影* 上下文 token(与 harness 右下角显示的同一
303
+ 数值,provider 锚定)。达到 `autoThresholdTokens` 时,拒绝发起模型请求,改为压缩头段,并逐字保留
304
+ 最新的 `retainLatestTokens`。
305
+ - **回合结束 / idle 压缩(`agent/status` → `idle`)** —— agent 静止(含子代理全部结束)时,可选地经
306
+ `compactNow` 压缩(开关:`turnEndForceCompactionEnabled`)。
307
+ - **手动 `/force-compact` 斜杠命令** —— 对忙/闲 agent 都能生效:空闲立即压缩;繁忙则排队一个
308
+ process‑local 强制标记,在下一个模型步骤消费。
309
+ - **`session/flush` 检查点** —— 等待型的持久化检查点。
310
+
311
+ 每条路径最终都汇入唯一的「**压缩结果落入会话**」边界——也正是**发送 liveUI 信令**的位置。
312
+
313
+ ### 判定基准是 *provider 锚定* 的
314
+
315
+ 判定使用 `projectedTokens`(与 UI 角标同款数值),插件因此永不偏离你所见的数字。重度 CJK /
316
+ tool‑JSON 内容按米表 chars/token 密度计价以保持口径一致;阈值感知的缩容门禁会跳过「注定无法把会话
317
+ 降到阈值以下」的摘要 LLM 调用(消灭低阈值死循环)。
318
+
319
+ ### 影子价格记账与米表对齐
320
+
321
+ 内置事务的 `shadowedTokenCount` 取自**与官方相同的** `tokenMeter.measure` 逐节点单价,使米表的折叠
322
+ 协议正确结算下降——压缩后右下角计数是**下降**而非漂移上涨。
323
+
324
+ ### 后端无关的思考控制
325
+
326
+ `disableThinking` 在**两个互补的缝**上强制执行:
327
+
328
+ 1. **请求缝** —— `reasoningEffort:'off'` → DeepSeek 适配器序列化为 `thinking:{type:'disabled'}`
329
+ (真 DeepSeek API 认这个字段)。
330
+ 2. **wire 缝(`llm/stream`)** —— 插件在序列化后追加顶层 `reasoning_effort:"none"`,llama.cpp 的
331
+ OpenAI 兼容层原生解析(`server‑common.cpp` 映射到 `enable_thinking=false`,与模板能力无关)。
332
+ 真 DeepSeek 端点忽略未知键。
333
+
334
+ 结果:在任何后端(包括本地 llama.cpp)上都**确实关闭了思考**,不依赖目标嗅探启发式而漏判路由。
335
+
336
+ ### LiveUI 状态
337
+
338
+ 一个极小的 host→client 信令通道(`liveUi` 设置字段,实时镜像到浏览器),在 turn 旁绘制徽标:
339
+
340
+ - 🟥 `compressing` —— 固定红字 `[强制压缩中>>>]`,在压缩提交前一刻发出;
341
+ - 🟢 `done` —— 固定绿字 `[压缩完成!]`,**在压缩结果落入会话的瞬间**发出,3 秒后回落为一组全新随机的
342
+ working 文案;
343
+ - 🔵 `working` —— 否则是一条玩梗式的随机短句("正在缝合上下文…"、"正在憋大招…")。
344
+
345
+ 发布器绝对安全:信令故障永远不会干扰真实压缩事务。
346
+
347
+ ---
348
+
349
+ ## 工作原理
350
+
351
+ 插件钩住官方的模型请求 Waterfall,使决策发生在**真正发起模型请求之前**,以及持久化检查点上:
352
+
353
+ - **`agent/request`** —— 围绕冻结调用配置的 Waterfall。`disableThinking` 开启时返回携带
354
+ `reasoningEffort:'off'` 的配置。参数**每次请求**读取,故 `settings.yaml` 改动下次请求即生效。
355
+ - **`agent/pre-step`** —— 每个模型步骤前的 Waterfall。读取 *投影* token,达到 `autoThresholdTokens`
356
+ 时返回 `{ kind:'reject' }`(不发起模型请求),并压缩头段、逐字保留最新 `retainLatestTokens`。
357
+ - **`session/flush`** —— 等待型 `parallel` 检查点,保证压缩在调用方继续前完成。
358
+ - **`/force-compact`** —— 斜杠命令,不把该行发送给模型:空闲立即 `compactNow`,繁忙排队强制标记。
359
+
360
+ ```
361
+ agent/request(payload, next) # 每次模型请求
362
+ disableThinking? -> { ...config, reasoningEffort: "off" }
363
+
364
+ agent/pre-step(payload, next) # 每个模型步骤前
365
+ projectedTokens >= autoThresholdTokens?
366
+ no -> next() # 放行模型请求
367
+ yes -> compactRegion(head-before-retainLatestTokens, signal)
368
+ return { kind: "reject" } # 本步不请求模型
369
+
370
+ agent/status({ agent, status }) # 生命周期过渡
371
+ status === "idle" && turnEndForceCompactionEnabled?
372
+ -> compactNow(agent, freshSignal) # 回合结束压缩
373
+
374
+ session/flush(session) # 持久化检查点
375
+ 选区 -> 投影消息 -> 预览 + 缩容门禁
376
+ -> compaction.compactRegion(start, end, agent, signal)
377
+ ```
378
+
379
+ 支撑模块:
380
+
381
+ - `src/hooks/guard.js` —— 每请求门禁:关思考 + 阈值门 + 强制标记。
382
+ - `src/hooks/command.js` —— `/force-compact` 命令。
383
+ - `src/hooks/idle.js` —— 回合结束强制压缩。
384
+ - `src/hooks/wire-rewrite.js` —— `llm/stream` wire 补丁,追加 `reasoning_effort:"none"`。
385
+ - `src/engine/region.js` —— 头/尾锚定的选区(含官方配对账本)。
386
+ - `src/engine/summarizer.js` —— 一次性 LLM 摘要器。
387
+ - `src/engine/builtin.js` —— 内置持久事务(官方 `compaction/*` 词汇)。
388
+ - `src/core/projected.js` —— provider 锚定的 `projectedTokens` 读取。
389
+ - `src/core/ui-signal.js` —— liveUI 信令器。
390
+
391
+ ---
392
+
393
+ ## 安装与验证
394
+
395
+ 作为可安装 bundle(推荐):
396
+
397
+ ```sh
398
+ # 从 git:
399
+ dsh plugin --profile web add github:falling-ts/dsh-force-compact
400
+ # 从本地 checkout:
401
+ dsh plugin --profile web add ./dsh-force-compact
402
+ ```
403
+
404
+ 或本地 checkout 不经安装、仅作 `--patch` 叠加:
405
+
406
+ ```sh
407
+ dsh web --patch dsh-force-compact/cordis.patch.yml
408
+ ```
409
+
410
+ 插件已加载 ⟺ `~/.dsh/logs/dsh-force-compact.log` 出现:
411
+
412
+ ```
413
+ [force-compact] debug logging enabled — writing [force-compact] lines to <绝对路径>
414
+ ```
415
+
416
+ 验证压缩确实发生:
417
+
418
+ ```
419
+ idle compaction (builtin) shadowed N nodes (~M tokens)
420
+ builtin compaction OK — replaced span seq[A..B] (N nodes, ~K tokens) with a P-char checkpoint
421
+ ```
422
+
423
+ ---
424
+
425
+ ## 配置(`$DSH_HOME/settings.yaml`,命名空间 `falling-ts-force-compact`)
426
+
427
+ | 键 | 类型 | 默认 | 含义 |
428
+ |----|------|------|------|
429
+ | `disableThinking` | boolean | `true` | 每次出站调用关闭模型推理努力(上述两缝)。 |
430
+ | `autoThresholdTokens` | number ≥ 32000 | `32000` | 每请求门禁的投影 token 阈值。越低越激进、上下文越瘦。**下限 32000**(存储值读取时抬升)。 |
431
+ | `retainLatestTokens` | 正整数 ≥ 8000 | `8000` | 逐字保留最新 N tokens;更早内容一次性发给摘要器。**下限 8000**。同时驱动自动门禁与 `/force-compact`。 |
432
+ | `turnEndForceCompactionEnabled` | boolean | `true` | 在 agent `idle` 过渡时压缩。 |
433
+ | `debug` | boolean | `true` | 输出 `[force-compact]` 诊断到插件日志。 |
434
+ | `logFile` | string | `~/.dsh/logs/dsh-force-compact.log` | 诊断输出路径(`~` 展开为用户家目录)。 |
435
+ | `compactionMode` | `'realm' \| 'global'` | `'realm'` | 官方服务解析策略(priority‑1 路径)。 |
436
+ | `builtinEnabled` | boolean | `true` | 内置引擎后备闸门。 |
437
+ | `maxSummaryTokens` | 整数 (1024–200000) | `1024` | 摘要 LLM 调用的 `maxTokens` 上限。 |
438
+
439
+ 示例——激进的**本地**配置:
440
+
441
+ ```yaml
442
+ falling-ts-force-compact:
443
+ disableThinking: true
444
+ autoThresholdTokens: 40000 # 更早压缩 ⇒ 常驻 prompt 更小
445
+ retainLatestTokens: 8000
446
+ turnEndForceCompactionEnabled: true
447
+ ```
448
+
449
+ 当 `settings` 服务缺席时,插件回退到相同默认值并照常压缩——该命名空间是可选的,绝不成为硬依赖。
450
+
451
+ ### 面向低上下文 llama.cpp 的调参建议
452
+
453
+ 用舒适但适中的上下文服务 Qwen3.8‑27B,把有效窗口交给插件决定:将 `autoThresholdTokens` 设在**明显
454
+ 低于**你服务的上下文,使常驻 prompt 保持小、延迟平稳,而 agent 仍通过被压缩的头段保留深层记忆。由于
455
+ 压力按 *投影* token(provider 锚定)度量,阈值会可预测地对应到你 UI 上看到的数字。
456
+
457
+ ---
458
+
459
+ ## 行为说明与限制
460
+
461
+ - **运行时依赖:** `compaction` 服务(preset 平面 `agent-presets:compaction-basic`)。经
462
+ `ctx.get('compaction')` 实时读取;不可用时强制压缩路径放行、让请求继续。
463
+ - **可选依赖:** `settings` / `tokenMeter` / `commands` / `llm` / `agents` 均经 `ctx.get(...)` 读取并
464
+ 守卫;缺任一都优雅降级而非崩溃。
465
+ - **每请求读参数:** 参数每次模型请求读取,故改动下次请求即生效、无需重启。
466
+ - **信号:** `agent/*` Waterfall 转发当前 turn 的 signal;`session/flush` 检查点与 `agent/status`
467
+ idle 监听器各自新建 `AbortController`。
468
+ - **持久性:** 持久产物为压缩括号事件 + 带 `surfaceOp:replace` 的 `user/message` 检查点,跨 build
469
+ 重放安全。
470
+ - **客户端半部:** `web/client.js` 新增设置分区 "强制压缩 / Force Compact",支持实时改值(uSES 安全的
471
+ 镜像,无 timer/状态)。
472
+ - **除一处外无 timer:** 唯一有意保留的是 3 s 的 `publishDone` 回落(纯表现层,已在文档声明)。其余均为
473
+ 纯监听器 + 一个 process‑local `Map` 强制标记。
474
+
475
+ ---
476
+
477
+ ## License
478
+
479
+ MIT(见 LICENSE)。