@falling-ts/dsh-force-compact 0.2.0 → 0.2.2

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/README.md CHANGED
@@ -1,174 +1,152 @@
1
1
  # dsh-force-compact
2
2
 
3
- **Local-first, aggressive context compaction for DeepSeek Harness agents.**
3
+ **Aggressive, local-first context compaction for DeepSeek Harness agents.**
4
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.
5
+ A DSH **Cordis function plugin** that keeps the agent's working context lean *by design*, so you
6
+ can deliver a **large-window experience** against a self-hosted llama.cpp serving `Qwen3.8‑27B`
7
+ at modest context — no API cost, no data egress.
9
8
 
10
- English | [中文](README.cn.md)
9
+ [中文](README.cn.md)
11
10
 
12
11
  ---
13
12
 
14
- ## Why run Qwen3.8‑27B locally on llama.cpp, at low context?
13
+ ## Why
15
14
 
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.**
15
+ Most harness setups bolt a big frontier model onto a short context budget. This plugin makes
16
+ the opposite bet: **you own the weights, the endpoint, and the context budget.**
18
17
 
19
- - **Selfhosted inference.** Point the agent at a local OpenAIcompatible llama.cpp server
18
+ - **Self-hosted inference.** Point the agent at a local OpenAI-compatible llama.cpp server
20
19
  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.
20
+ adapter path — no separate llama.cpp adapter needed).
21
+ - **Low context, high signal.** Rather than fight a small hard cap, the plugin **shrinks the
22
+ conversation itself**, so the agent reasons over a tight, high-signal prompt while effectively
23
+ reaching a much larger working memory.
24
+ - **Thinking-off by default.** `disableThinking: true` turns off reasoning effort on **every**
25
+ outbound call, enforced at two complementary seams (real DeepSeek honors one; llama.cpp honors
26
+ the other see "Backend-agnostic thinking control" below).
27
+ - **Private & free.** No per-token billing, no egress, and the exact model/context tradeoff is
28
+ yours to dial.
38
29
 
39
30
  ---
40
31
 
41
- ## What the plugin does
32
+ ## What it does
42
33
 
43
34
  Two compaction engines coexist behind one facade (`resolveCompaction`), transparent to callers:
44
35
 
45
- | Engine | When used | Notes |
36
+ | Engine | Used when | Notes |
46
37
  |--------|-----------|-------|
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 realmisolated (typical standard preset) | Selfcontained persistent transaction using only `ctx.sessions` / `ctx.llm.stream` / `ctx.tokenMeter`. Reuses the official `compaction/*` event vocabulary, so it survives crossbuild replay with no `ignorable` hacks. |
38
+ | **Official** | `compaction` service is resolvable in the agent realm | Preferred; delegates to `compaction/basic`. |
39
+ | **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
40
 
50
- You never toggle between them — official wins when reachable, builtin takes over otherwise.
41
+ You never toggle — official wins when reachable, builtin takes over otherwise.
51
42
 
52
43
  ### Trigger points
53
44
 
54
- - **Perrequest guard (`agent/pre-step`)** — reads the session's *projected* context tokens (the
55
- exact number the harness renders bottomright, provideranchored). When it reaches
56
- `autoThresholdTokens`, it rejects the outgoing model request and compacts the head instead,
57
- retaining the latest `retainLatestTokens` verbatim.
58
- - **Turnend / 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.
45
+ - **Per-request guard (`agent/pre-step`)** — reads the session's *projected* context tokens
46
+ (the exact number the harness renders bottom-right, provider-anchored). At
47
+ `autoThresholdTokens` it rejects the outgoing request and compacts the head instead, retaining
48
+ the latest `retainLatestTokens` verbatim.
49
+ - **Turn-end / idle (`agent/status` → `idle`)** — when the agent quiesces, optionally compacts
50
+ via `compactNow` (gate: `turnEndForceCompactionEnabled`).
51
+ - **Manual `/force-compact`** — immediate `compactNow` when idle; queues a process-local force
52
+ flag consumed at the next model step when busy.
53
+ - **`session/flush`** the awaited durability checkpoint.
65
54
 
66
55
  Every path funnels into the single *"compaction result landed in the session"* boundary — the
67
- same place the live UI signal is emitted (below).
56
+ same point where the live-UI signal fires.
68
57
 
69
- ### Decision basis is *provider‑anchored*
58
+ ### Provider-anchored decisions
70
59
 
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).
60
+ Decisions key off `projectedTokens` (same figure shown in the UI corner), so the plugin never
61
+ drifts from what you see. Threshold-aware shrink gates skip summarizer calls that provably could
62
+ not pull the session below the threshold (kills the low-threshold dead loop).
76
63
 
77
- ### Shadow‑price accounting aligned with the meter
64
+ ### Meter-aligned shadow-price billing
78
65
 
79
66
  The builtin transaction bills `shadowedTokenCount` from the **same** `tokenMeter.measure`
80
- pernode prices the official engine uses, so the meter's collapse protocol settles the drop
81
- correctly — the bottomright counter goes *down* after compaction instead of drifting upward.
67
+ per-node prices the official engine uses, so the meter's collapse protocol settles the drop
68
+ correctly — the bottom-right counter goes *down* after compaction instead of drifting upward.
82
69
 
83
- ### Backendagnostic thinking control
70
+ ### Backend-agnostic thinking control
84
71
 
85
72
  `disableThinking` is enforced at **two complementary seams**:
86
73
 
87
74
  1. **Request seam** — `reasoningEffort:'off'` → the DeepSeek adapter serializes
88
75
  `thinking:{type:'disabled'}` (real DeepSeek APIs honor it).
89
- 2. **Wire seam (`llm/stream`)** — the plugin appends toplevel `reasoning_effort:"none"`
90
- postserialization, which llama.cpp's OpenAIcompatible layer parses natively
91
- (`servercommon.cpp` maps it to `enable_thinking=false` regardless of template capability).
76
+ 2. **Wire seam (`llm/stream`)** — the plugin appends top-level `reasoning_effort:"none"`
77
+ post-serialization, which llama.cpp's OpenAI-compatible layer parses natively
78
+ (`server-common.cpp` maps it to `enable_thinking=false`, independent of template capability).
92
79
  Real DeepSeek endpoints simply ignore the unknown key.
93
80
 
94
- Result: thinking is genuinely off on **any** backend including your local llama.cpp — with no
95
- target‑sniffing heuristic to miss a route.
81
+ Result: thinking is genuinely off on **any** backend, with no target-sniffing heuristic to miss
82
+ a route.
96
83
 
97
- ### Live UI status
84
+ ### Live-UI status
98
85
 
99
- A tiny host→client messenger (a `liveUi` settings field, mirrored live to the browser) paints a
86
+ A tiny host→client messenger (a `liveUi` settings field mirrored live to the browser) paints a
100
87
  badge beside the turn:
101
88
 
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 oneliner ("正在缝合上下文…", "正在憋大招…").
89
+ - **🟥 compressing** — pinned red `[强制压缩中>>>]`, fired just before a compaction commits;
90
+ - **🟢 done** — pinned green `[压缩完成!]`, fired the instant a compaction result lands; 3 s
91
+ later a fresh random "working" pair takes over;
92
+ - **🔵 working** — otherwise a playful random one-liner ("正在缝合上下文…", "正在憋大招…").
106
93
 
107
- Publishers are failsafe: a messenger glitch can never disturb the actual compaction.
94
+ Publishers are fail-safe: a messenger glitch can never disturb the actual compaction.
108
95
 
109
96
  ---
110
97
 
111
98
  ## How it works
112
99
 
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
100
  ```
128
101
  agent/request(payload, next) # every model request
129
102
  disableThinking? -> { ...config, reasoningEffort: "off" }
130
103
 
131
104
  agent/pre-step(payload, next) # before each model step
132
105
  projectedTokens >= autoThresholdTokens?
133
- no -> next() # let the model request proceed
106
+ no -> next() # let the model request proceed
134
107
  yes -> compactRegion(head-before-retainLatestTokens, signal)
135
- return { kind: "reject" } # NO model request this step
108
+ return { kind: "reject" } # no model request this step
136
109
 
137
- agent/status({ agent, status }) # lifecycle transition
110
+ agent/status({ agent, status }) # lifecycle transition
138
111
  status === "idle" && turnEndForceCompactionEnabled?
139
- -> compactNow(agent, freshSignal) # turn-end compaction
112
+ -> compactNow(agent, freshSignal) # turn-end compaction
140
113
 
141
- session/flush(session) # durability checkpoint
114
+ session/flush(session) # durability checkpoint
142
115
  select region -> project messages -> preview + shrink gate
143
116
  -> compaction.compactRegion(start, end, agent, signal)
144
117
  ```
145
118
 
146
119
  Supporting modules:
147
120
 
148
- - `src/hooks/guard.js` — perrequest guard: thinkingoff + threshold gate + forced flag.
121
+ - `src/hooks/guard.js` — per-request guard: thinking-off + threshold gate + forced flag.
149
122
  - `src/hooks/command.js` — the `/force-compact` command.
150
- - `src/hooks/idle.js` — turnend forced compaction.
123
+ - `src/hooks/idle.js` — turn-end forced compaction.
151
124
  - `src/hooks/wire-rewrite.js` — the `llm/stream` wire patch appending `reasoning_effort:"none"`.
152
- - `src/engine/region.js` — head/tailanchored region selection (+ official pairing ledger).
153
- - `src/engine/summarizer.js` — the oneshot LLM summarizer.
125
+ - `src/engine/region.js` — head/tail-anchored region selection (with the official pairing ledger).
126
+ - `src/engine/summarizer.js` — the one-shot LLM summarizer (fully aligned with official
127
+ `compaction-basic`: target resolution, prefix-cache alignment, `purpose:'compaction'` tag,
128
+ fail-closed finish classification, usage capture).
154
129
  - `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.
130
+ - `src/engine/checkpoint.js` — preview + shrink gate + delegation to the compaction service.
131
+ - `src/core/projected.js` — provider-anchored `projectedTokens` reading.
132
+ - `src/core/ui-signal.js` — the live-UI messenger.
157
133
 
158
134
  ---
159
135
 
160
- ## Install & verify
136
+ ## Install
161
137
 
162
138
  As an installable bundle (recommended):
163
139
 
164
140
  ```sh
141
+ # from npm (published):
142
+ npm install @falling-ts/dsh-force-compact
165
143
  # from git:
166
144
  dsh plugin --profile web add github:falling-ts/dsh-force-compact
167
145
  # from a local checkout:
168
146
  dsh plugin --profile web add ./dsh-force-compact
169
147
  ```
170
148
 
171
- or, from a local checkout, as a `--patch` overlay without installing:
149
+ Or, from a local checkout, as a `--patch` overlay without installing:
172
150
 
173
151
  ```sh
174
152
  dsh web --patch dsh-force-compact/cordis.patch.yml
@@ -189,17 +167,19 @@ builtin compaction OK — replaced span seq[A..B] (N nodes, ~K tokens) with a P-
189
167
 
190
168
  ---
191
169
 
192
- ## Settings (`$DSH_HOME/settings.yaml`, namespace `falling-ts-force-compact`)
170
+ ## Settings
171
+
172
+ `$DSH_HOME/settings.yaml`, namespace `falling-ts-force-compact`:
193
173
 
194
174
  | key | type | default | meaning |
195
175
  |-----|------|---------|---------|
196
- | `disableThinking` | boolean | `true` | Disable model reasoning effort on **every** outbound call (both seams above). |
197
- | `autoThresholdTokens` | number ≥ 32000 | `32000` | Projectedtoken trigger for the perrequest 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. |
176
+ | `disableThinking` | boolean | `true` | Disable reasoning effort on **every** outbound call (both seams above). |
177
+ | `autoThresholdTokens` | number ≥ 32000 | `32000` | Projected-token trigger for the per-request gate. Lower ⇒ more aggressive. **Floor 32000** (stored values clamp back up at read time). |
178
+ | `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
179
  | `turnEndForceCompactionEnabled` | boolean | `true` | Compact on the agent's `idle` transition. |
200
180
  | `debug` | boolean | `true` | Emit `[force-compact]` diagnostics to the plugin log. |
201
181
  | `logFile` | string | `~/.dsh/logs/dsh-force-compact.log` | Diagnostics destination (`~` expands to home dir). |
202
- | `compactionMode` | `'realm' \| 'global'` | `'realm'` | Officialservice resolution strategy (priority1 path). |
182
+ | `compactionMode` | `'realm' \| 'global'` | `'realm'` | Official-service resolution strategy (priority-1 path). |
203
183
  | `builtinEnabled` | boolean | `true` | Gate for the builtin engine fallback. |
204
184
  | `maxSummaryTokens` | integer (1024–200000) | `1024` | Cap on the summarizer LLM `maxTokens`. |
205
185
 
@@ -216,264 +196,36 @@ falling-ts-force-compact:
216
196
  When the `settings` service is absent, the plugin falls back to the same defaults and still
217
197
  compacts — the namespace is optional, never a hard dependency.
218
198
 
219
- ### Tuning for lowcontext llama.cpp
199
+ ### Tuning for low-context llama.cpp
220
200
 
221
- Serve Qwen3.8‑27B with a comfortablebutmodest context, then let the plugin decide the
222
- effective window: keep `autoThresholdTokens` comfortably **below** your served context so the
201
+ Serve `Qwen3.8‑27B` with a comfortable-but-modest context and let the plugin decide the
202
+ effective window: keep `autoThresholdTokens` comfortably **below** the served context so the
223
203
  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 (provideranchored), the
204
+ compressed head. Pressure is measured in *projected* tokens (provider-anchored), so the
225
205
  threshold maps predictably onto what the UI shows you.
226
206
 
227
207
  ---
228
208
 
229
- ## Behavior notes & limitations
209
+ ## Behavior notes
230
210
 
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
- - **Perrequest settings read:** parameters are read per model request, so edits take effect on
211
+ - **Runtime dependency:** the `compaction` service (preset plane `agent-presets:compaction-basic`).
212
+ Read live via `ctx.get('compaction')`; when unreachable the forced-compaction path falls
213
+ through and lets the request proceed.
214
+ - **Optional dependencies:** `settings` / `tokenMeter` / `commands` / `llm` / `agents` are read
215
+ via `ctx.get(...)` with guards — a missing one degrades gracefully rather than crashing.
216
+ - **Per-request settings read:** parameters are read per model request, so edits take effect on
237
217
  the next request without a restart.
238
218
  - **Signals:** the `agent/*` Waterfalls forward the current turn's signal; the `session/flush`
239
219
  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, replaysafe across builds.
242
- - **Client half:** `web/client.js` adds a Settings section "强制压缩 / Force Compact" for editing
243
- these values live (uSESsafe 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.
220
+ - **Persistence:** the durable output is the `compaction/*` bracket events + a
221
+ `surfaceOp:replace` `user/message` checkpoint, replay-safe across builds.
222
+ - **Client half:** `web/client.js` adds a Settings section "强制压缩 / Force Compact" for
223
+ editing these values live (uSES-safe mirror, no timers/state).
224
+ - **One intentional timer:** the 3 s `publishDone` fallback (presentation-only, documented
225
+ deviation). Otherwise the plugin is pure listeners + a process-local `Map` force flag.
247
226
 
248
227
  ---
249
228
 
250
229
  ## License
251
230
 
252
231
  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)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@falling-ts/dsh-force-compact",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "DSH Cordis plugin: hooks the core model-request seam (agent/pre-step + agent/request) to force-compact a session's context and disable thinking per the \"强制压缩配置\" settings namespace (disableThinking, autoThresholdTokens, retainLatestTokens, turnEndForceCompactionEnabled). When the threshold fires (or /force-compact queues while busy), the latest `retainLatestTokens` of the conversation's surface tokens are KEPT VERBATIM and everything before that cutoff is COMPACTED INTO A SINGLE SUMMARY NODE in one LLM call (original span entries become shadowed/skipped). Also compacts at each turn/end and at each session/flush durability checkpoint. Host half is a pure listener; a web client half registers a settings.section (强制压缩 / Force Compact) that reads and writes the same settings namespace.",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -52,10 +52,10 @@ export const PINNED_TEXTS = Object.freeze({
52
52
  [PHASE_DONE]: '[压缩完成!]',
53
53
  })
54
54
 
55
- /** Pinned colors matching {@link PINNED_TEXTS} — red while compacting, green on completion. */
55
+ /** Pinned colors matching {@link PINNED_TEXTS} — extra-dark tuned: deep burgundy-red while compacting, muted pine-green on completion. */
56
56
  export const PINNED_COLORS = Object.freeze({
57
- [PHASE_COMPRESSING]: '#ff4d4f',
58
- [PHASE_DONE]: '#52c41a',
57
+ [PHASE_COMPRESSING]: '#9b1c2b',
58
+ [PHASE_DONE]: '#2f6f52',
59
59
  })
60
60
 
61
61
  /**
@@ -97,27 +97,38 @@ export const WORKING_TEXTS = Object.freeze([
97
97
  * so repeated draws visibly vary BOTH dimensions).
98
98
  * @readonly
99
99
  */
100
+ /**
101
+ * Extra-dark 20-color WORKING-phase palette (second darkening pass). Each
102
+ * entry sits one brightness step deeper than the prior dark-tuned set while
103
+ * preserving the full hue-wheel sweep (blue → indigo → violet → purple →
104
+ * plum → orchid → magenta → fuchsia → pink → rose → crimson → scarlet →
105
+ * vermilion → rust → ochre → gold → olive → moss → pine → fir → teal →
106
+ * cyan → azure → cobalt → navy). Saturation is held high enough that the
107
+ * badge reads as a distinct hue rather than desaturating toward grey.
108
+ * Random pairing with {@link WORKING_TEXTS} remains unchanged.
109
+ * @readonly
110
+ */
100
111
  export const WORKING_COLORS = Object.freeze([
101
- '#4f9cf9',
102
- '#5b8def',
103
- '#6a5bff',
104
- '#8b5cf6',
105
- '#a855f7',
106
- '#c45bf9',
107
- '#db6bd4',
108
- '#e86bb0',
109
- '#f06b8b',
110
- '#f76b5b',
111
- '#fb8c5b',
112
- '#fca95b',
113
- '#fdc35b',
114
- '#d8e05b',
115
- '#aede5b',
116
- '#7ee083',
117
- '#5be0a0',
118
- '#5becd8',
119
- '#5bcdf9',
120
- '#7ba8f9',
112
+ '#1e40af', // royal blue
113
+ '#1e3a8a', // deep blue
114
+ '#312e81', // indigo
115
+ '#4c1d95', // violet
116
+ '#581c87', // purple
117
+ '#8318a3', // plum
118
+ '#86198f', // orchid
119
+ '#9d174d', // magenta
120
+ '#9f1239', // pink
121
+ '#991b1b', // rose
122
+ '#9a3412', // crimson
123
+ '#92400e', // scarlet
124
+ '#854d0e', // rust
125
+ '#4d7c0f', // ochre-gold
126
+ '#3f6212', // olive
127
+ '#166534', // moss
128
+ '#065f46', // pine
129
+ '#0e7490', // teal
130
+ '#155e75', // cyan
131
+ '#172554', // navy
121
132
  ])
122
133
 
123
134
  /**