@falling-ts/dsh-force-compact 0.2.3 → 0.2.4
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.cn.md +161 -114
- package/README.md +149 -98
- package/index.js +41 -34
- package/package.json +1 -1
- package/src/engine/builtin.js +6 -0
- package/src/engine/summarizer.js +51 -0
- package/src/hooks/guard.js +19 -8
- package/src/hooks/wire-rewrite.js +105 -97
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
**Aggressive, local-first context compaction for DeepSeek Harness agents.**
|
|
4
4
|
|
|
5
|
-
A DSH **Cordis function plugin** that keeps the agent's working context lean
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
A DSH **Cordis function plugin** that keeps the agent's working context lean by design: serve
|
|
6
|
+
`Qwen3.8‑27B` on a self-hosted llama.cpp with a modest context, and the plugin shrinks the
|
|
7
|
+
conversation itself — a large-window feel with no API cost and no data egress.
|
|
8
8
|
|
|
9
9
|
[中文](README.cn.md)
|
|
10
10
|
|
|
@@ -12,20 +12,15 @@ at modest context — no API cost, no data egress.
|
|
|
12
12
|
|
|
13
13
|
## Why
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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.
|
|
15
|
+
- **Self-hosted inference** — the agent talks to a local OpenAI-compatible llama.cpp server
|
|
16
|
+
through the standard DeepSeek adapter; no separate adapter needed.
|
|
17
|
+
- **Low context, high signal** — instead of fighting a small cap, the plugin shrinks the
|
|
18
|
+
conversation, so the agent reasons over a tight prompt while keeping deep memory in the
|
|
19
|
+
compressed head.
|
|
20
|
+
- **Think off for compactions, passthrough everywhere else** — `disableThinking: true`
|
|
21
|
+
(default) turns thinking off on *this plugin's own compaction summarization call only*;
|
|
22
|
+
every other model request rides the machine's configuration unchanged.
|
|
23
|
+
- **Private & free** — no per-token billing, no egress.
|
|
29
24
|
|
|
30
25
|
---
|
|
31
26
|
|
|
@@ -35,61 +30,96 @@ Two compaction engines coexist behind one facade (`resolveCompaction`), transpar
|
|
|
35
30
|
|
|
36
31
|
| Engine | Used when | Notes |
|
|
37
32
|
|--------|-----------|-------|
|
|
38
|
-
| **Official** | `compaction` service
|
|
39
|
-
| **Builtin** |
|
|
33
|
+
| **Official** | the `compaction` service resolves in the agent realm | Preferred; delegates to `compaction/basic`. |
|
|
34
|
+
| **Builtin** | automatic fallback (typical standard preset isolates the service) | Self-contained persistent transaction on `ctx.sessions` / `ctx.llm.stream` / `ctx.tokenMeter`; reuses the official `compaction/*` event vocabulary, so it replays safely across builds. |
|
|
40
35
|
|
|
41
|
-
|
|
36
|
+
No toggling — official wins when reachable, builtin takes over otherwise.
|
|
42
37
|
|
|
43
38
|
### Trigger points
|
|
44
39
|
|
|
45
40
|
- **Per-request guard (`agent/pre-step`)** — reads the session's *projected* context tokens
|
|
46
|
-
(the exact number the harness renders bottom-right
|
|
47
|
-
|
|
48
|
-
the
|
|
49
|
-
- **Turn
|
|
50
|
-
via `compactNow` (gate: `turnEndForceCompactionEnabled`).
|
|
51
|
-
- **Manual `/force-compact`** — immediate `compactNow` when idle; queues a
|
|
52
|
-
flag consumed at the next model step
|
|
41
|
+
(the exact number the harness renders bottom-right). At `autoThresholdTokens` it rejects the
|
|
42
|
+
outgoing request and compacts the head instead, retaining the latest `retainLatestTokens`
|
|
43
|
+
verbatim. Below the threshold the request proceeds.
|
|
44
|
+
- **Turn end / idle (`agent/status` → `idle`)** — when the agent quiesces, optionally
|
|
45
|
+
compacts via `compactNow` (gate: `turnEndForceCompactionEnabled`).
|
|
46
|
+
- **Manual `/force-compact`** — immediate `compactNow` when idle; when busy it queues a
|
|
47
|
+
process-local flag consumed at the next model step. **Loaded lazily** — see "Command
|
|
48
|
+
availability" under Install.
|
|
53
49
|
- **`session/flush`** — the awaited durability checkpoint.
|
|
54
50
|
|
|
55
51
|
Every path funnels into the single *"compaction result landed in the session"* boundary — the
|
|
56
|
-
same point where the
|
|
52
|
+
same point where the LiveUI signal fires.
|
|
57
53
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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).
|
|
63
|
-
|
|
64
|
-
### Meter-aligned shadow-price billing
|
|
54
|
+
Decision key is `projectedTokens` (provider-anchored, same figure as the UI corner), so the
|
|
55
|
+
plugin never drifts from what you see; the threshold-aware shrink gate skips summarizer calls
|
|
56
|
+
that provably cannot pull the session below the threshold (kills the low-threshold dead loop).
|
|
65
57
|
|
|
66
58
|
The builtin transaction bills `shadowedTokenCount` from the **same** `tokenMeter.measure`
|
|
67
59
|
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
|
|
60
|
+
correctly — the bottom-right counter goes *down* after compaction.
|
|
61
|
+
|
|
62
|
+
### Thinking control: scoped to compactions
|
|
63
|
+
|
|
64
|
+
Since the 2026-08 semantics revision, `disableThinking` controls **one thing**: whether this
|
|
65
|
+
plugin's own summarization call (`engine/builtin.js` → `engine/summarizer.js` →
|
|
66
|
+
`ctx.llm.stream`) carries `reasoningEffort:'off'`. Everything else is untouched:
|
|
67
|
+
|
|
68
|
+
| Call site | With `disableThinking: true` |
|
|
69
|
+
|---|---|
|
|
70
|
+
| Builtin-engine summarization call | Carries `reasoningEffort:'off'` |
|
|
71
|
+
| Every other model request (business, sub-agents, tools, other plugins) | Machine's `LlmCallConfig` unchanged |
|
|
72
|
+
| Official `compaction` service calls | Not routed through any plugin seam — unaffected |
|
|
73
|
+
|
|
74
|
+
When the target is a **llama.cpp / OpenAI-compatible endpoint**, the
|
|
75
|
+
`thinking: { type: 'disabled' }` field the adapter emits is silently ignored there — so the
|
|
76
|
+
summarizer ALSO stamps the llama.cpp-native top-level `reasoning_effort: "none"`, gated on
|
|
77
|
+
the exact same condition. One options object carries BOTH fields:
|
|
78
|
+
|
|
79
|
+
| Endpoint family | Reads | Result |
|
|
80
|
+
|---|---|---|
|
|
81
|
+
| Real DeepSeek API | `reasoningEffort:'off'` → `thinking:{type:'disabled'}` | Thinking off ✅ |
|
|
82
|
+
| llama.cpp / OAI-compatible | `reasoning_effort:"none"` (top-level) | `enable_thinking=false` ✅ |
|
|
83
|
+
|
|
84
|
+
Each family tolerates-and-ignores the foreign key, so emitting both is harmless. The field is
|
|
85
|
+
stamped in `src/engine/summarizer.js` (immediately before `llm.stream(options)`), NOT in the
|
|
86
|
+
`llm/stream` waterfall — a prior draft injected there but proved ineffective structurally
|
|
87
|
+
(middle-layer returns are discarded; in-place seed mutation crashes the host); see the
|
|
88
|
+
`src/hooks/wire-rewrite.js` module header for the write-up. That hook now serves only the
|
|
89
|
+
LiveUI watermark role.
|
|
69
90
|
|
|
70
|
-
|
|
91
|
+
Need thinking off on **business calls** too? Set your provider's `reasoningEffort` at the
|
|
92
|
+
request-header level — the plugin deliberately stays out of that decision.
|
|
71
93
|
|
|
72
|
-
|
|
94
|
+
#### Observability: per-attempt audit lines
|
|
73
95
|
|
|
74
|
-
|
|
75
|
-
|
|
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).
|
|
79
|
-
Real DeepSeek endpoints simply ignore the unknown key.
|
|
96
|
+
Every summarization attempt logs two lines (visible at the default `debug: true`) — the
|
|
97
|
+
durable proof of the scoping decision and its wire fields, without capturing traffic:
|
|
80
98
|
|
|
81
|
-
|
|
82
|
-
|
|
99
|
+
```
|
|
100
|
+
[force-compact] <sessionId>: compaction thinking-policy — settings.disableThinking=true → extra.reasoningEffort='off' (this summarization call carries thinking-OFF)
|
|
101
|
+
[force-compact] <sessionId>: summarization wire-fields → <provider>/<model>: reasoningEffort='off' + reasoning_effort="none" (llama.cpp-native wire field)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
- **Line 1** (`engine/builtin.js`) records where `disableThinking` is read and routed into
|
|
105
|
+
the call options; with the setting off it records *machine default*.
|
|
106
|
+
- **Line 2** (`engine/summarizer.js`) records both wire fields exactly as they leave the
|
|
107
|
+
options object, plus resolved provider/model; unstamped fields are labeled `(absent…)`.
|
|
108
|
+
|
|
109
|
+
Empirically grounded: probed against a local llama.cpp endpoint, a baseline request returned
|
|
110
|
+
populated `reasoning_content` (the model thinks by default), while the same request with
|
|
111
|
+
top-level `reasoning_effort:"none"` returned none at all — the field genuinely disables
|
|
112
|
+
thinking there, and business calls (which omit it) keep thinking.
|
|
83
113
|
|
|
84
|
-
###
|
|
114
|
+
### LiveUI status
|
|
85
115
|
|
|
86
|
-
A tiny host→client messenger (
|
|
116
|
+
A tiny host→client messenger (the `liveUi` settings field mirrored live to the browser) pins a
|
|
87
117
|
badge beside the turn:
|
|
88
118
|
|
|
89
|
-
-
|
|
90
|
-
-
|
|
91
|
-
|
|
92
|
-
-
|
|
119
|
+
- **Red "compressing"** — just before a compaction commits (screen text is Chinese);
|
|
120
|
+
- **Green "done"** — the instant a compaction lands; 3 s later a fresh random working line
|
|
121
|
+
takes over;
|
|
122
|
+
- **Blue "working"** — otherwise a rotating playful one-liner.
|
|
93
123
|
|
|
94
124
|
Publishers are fail-safe: a messenger glitch can never disturb the actual compaction.
|
|
95
125
|
|
|
@@ -99,13 +129,14 @@ Publishers are fail-safe: a messenger glitch can never disturb the actual compac
|
|
|
99
129
|
|
|
100
130
|
```
|
|
101
131
|
agent/request(payload, next) # every model request
|
|
102
|
-
|
|
132
|
+
return await next() # pure pass-through (thinking-off scopes
|
|
133
|
+
# ONLY to the plugin's own summarizer)
|
|
103
134
|
|
|
104
135
|
agent/pre-step(payload, next) # before each model step
|
|
105
136
|
projectedTokens >= autoThresholdTokens?
|
|
106
|
-
no -> next() # let the
|
|
137
|
+
no -> next() # let the request proceed
|
|
107
138
|
yes -> compactRegion(head-before-retainLatestTokens, signal)
|
|
108
|
-
|
|
139
|
+
return { kind: "reject" } # no model request this step
|
|
109
140
|
|
|
110
141
|
agent/status({ agent, status }) # lifecycle transition
|
|
111
142
|
status === "idle" && turnEndForceCompactionEnabled?
|
|
@@ -118,24 +149,26 @@ session/flush(session) # durability checkpoint
|
|
|
118
149
|
|
|
119
150
|
Supporting modules:
|
|
120
151
|
|
|
121
|
-
- `src/hooks/guard.js` —
|
|
122
|
-
- `
|
|
152
|
+
- `src/hooks/guard.js` — `agent/request` pure pass-through + `pre-step` threshold gate +
|
|
153
|
+
process-local force flag (`thinkingDisabled` survives only as a legacy predicate).
|
|
154
|
+
- `src/hooks/command.js` — the `/force-compact` command (lazily registered).
|
|
123
155
|
- `src/hooks/idle.js` — turn-end forced compaction.
|
|
124
|
-
- `src/hooks/wire-rewrite.js` — the `llm/stream`
|
|
156
|
+
- `src/hooks/wire-rewrite.js` — the `llm/stream` LiveUI watermark hook (no wire manipulation;
|
|
157
|
+
historical note in the module header).
|
|
125
158
|
- `src/engine/region.js` — head/tail-anchored region selection (with the official pairing ledger).
|
|
126
|
-
- `src/engine/summarizer.js` — the one-shot LLM summarizer
|
|
127
|
-
`compaction-basic
|
|
159
|
+
- `src/engine/summarizer.js` — the one-shot LLM summarizer, fully aligned with official
|
|
160
|
+
`compaction-basic` (target resolution, prefix-cache alignment, `purpose:'compaction'` tag,
|
|
128
161
|
fail-closed finish classification, usage capture).
|
|
129
162
|
- `src/engine/builtin.js` — the builtin persistent transaction (official `compaction/*` vocab).
|
|
130
163
|
- `src/engine/checkpoint.js` — preview + shrink gate + delegation to the compaction service.
|
|
131
|
-
- `src/core/projected.js` — provider-anchored `projectedTokens
|
|
132
|
-
- `src/core/ui-signal.js` — the
|
|
164
|
+
- `src/core/projected.js` — provider-anchored `projectedTokens`.
|
|
165
|
+
- `src/core/ui-signal.js` — the LiveUI messenger.
|
|
133
166
|
|
|
134
167
|
---
|
|
135
168
|
|
|
136
169
|
## Install
|
|
137
170
|
|
|
138
|
-
As an installable
|
|
171
|
+
As an installable package (recommended):
|
|
139
172
|
|
|
140
173
|
```sh
|
|
141
174
|
# from npm (published):
|
|
@@ -152,19 +185,37 @@ Or, from a local checkout, as a `--patch` overlay without installing:
|
|
|
152
185
|
dsh web --patch dsh-force-compact/cordis.patch.yml
|
|
153
186
|
```
|
|
154
187
|
|
|
155
|
-
|
|
188
|
+
The plugin is loaded iff `~/.dsh/logs/dsh-force-compact.log` gains:
|
|
156
189
|
|
|
157
190
|
```
|
|
158
191
|
[force-compact] debug logging enabled — writing [force-compact] lines to <absolute path>
|
|
159
192
|
```
|
|
160
193
|
|
|
194
|
+
### Command availability — `/force-compact` loads lazily
|
|
195
|
+
|
|
196
|
+
The `commands` service arrives with the agent-presets plane, **after** the plugin's boot-time
|
|
197
|
+
`apply`, so registration happens at the first guarded-listener activation
|
|
198
|
+
(`agent/request` / `agent/pre-step` / `agent/status` / `session/flush`), settling
|
|
199
|
+
permanently on the first success. Practical effect: **after (re)starting the instance, a
|
|
200
|
+
fresh session's `/` picker does NOT show `/force-compact` until that session makes its first
|
|
201
|
+
model request** — send any one message, then the command is registered process-wide.
|
|
202
|
+
|
|
203
|
+
- Success: `[force-compact] /force-compact command registered (deferred)`
|
|
204
|
+
- `commands` permanently absent: one `… still UNREGISTERED 10 min …` warn explains the
|
|
205
|
+
empty picker. Until registered, the rest of the plugin works — degradation, not an
|
|
206
|
+
install failure.
|
|
207
|
+
|
|
161
208
|
Verify a compaction happened:
|
|
162
209
|
|
|
163
210
|
```
|
|
164
211
|
idle compaction (builtin) shadowed N nodes (~M tokens)
|
|
165
212
|
builtin compaction OK — replaced span seq[A..B] (N nodes, ~K tokens) with a P-char checkpoint
|
|
213
|
+
compaction thinking-policy — settings.disableThinking=true → extra.reasoningEffort='off' (…)
|
|
214
|
+
summarization wire-fields → <provider>/<model>: reasoningEffort='off' + reasoning_effort="none" (…)
|
|
166
215
|
```
|
|
167
216
|
|
|
217
|
+
(The last two lines are the per-attempt audit pair described under "Observability".)
|
|
218
|
+
|
|
168
219
|
---
|
|
169
220
|
|
|
170
221
|
## Settings
|
|
@@ -173,9 +224,9 @@ builtin compaction OK — replaced span seq[A..B] (N nodes, ~K tokens) with a P-
|
|
|
173
224
|
|
|
174
225
|
| key | type | default | meaning |
|
|
175
226
|
|-----|------|---------|---------|
|
|
176
|
-
| `disableThinking` | boolean | `true` |
|
|
177
|
-
| `autoThresholdTokens` | number ≥ 32000 | `32000` | Projected-token trigger for the
|
|
178
|
-
| `retainLatestTokens` | positive int ≥ 8000 | `8000` | Retain the latest N tokens verbatim;
|
|
227
|
+
| `disableThinking` | boolean | `true` | Only the plugin's own summarization call carries `reasoningEffort:'off'`; everything else unchanged. |
|
|
228
|
+
| `autoThresholdTokens` | number ≥ 32000 | `32000` | Projected-token trigger for the gate. **Floor 32000** (clamps back up at read time). |
|
|
229
|
+
| `retainLatestTokens` | positive int ≥ 8000 | `8000` | Retain the latest N tokens verbatim; older history is summarized in one batch. **Floor 8000.** Drives both the auto gate and `/force-compact`. |
|
|
179
230
|
| `turnEndForceCompactionEnabled` | boolean | `true` | Compact on the agent's `idle` transition. |
|
|
180
231
|
| `debug` | boolean | `true` | Emit `[force-compact]` diagnostics to the plugin log. |
|
|
181
232
|
| `logFile` | string | `~/.dsh/logs/dsh-force-compact.log` | Diagnostics destination (`~` expands to home dir). |
|
|
@@ -193,52 +244,52 @@ falling-ts-force-compact:
|
|
|
193
244
|
turnEndForceCompactionEnabled: true
|
|
194
245
|
```
|
|
195
246
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
---
|
|
200
|
-
|
|
201
|
-
## Screenshots
|
|
202
|
-
|
|
203
|
-

|
|
204
|
-
|
|
205
|
-
*Settings panel — `设置 > 强制压缩`. All nine fields above can be edited live without restart.*
|
|
206
|
-
|
|
207
|
-
![Live conversation — red "[forced compacting>>>]" badge pinned beside an in-flight turn](assets/live-conversation.png)
|
|
208
|
-
|
|
209
|
-
*Conversation page — the live-UI signal paints three states (🟥 compressing / 🟢 done / 🔵 working);
|
|
210
|
-
the green banner fades after ~3 s back to a random "working" line.*
|
|
247
|
+
Without the `settings` service the plugin falls back to the same defaults and still compacts —
|
|
248
|
+
the namespace is optional, never a hard dependency.
|
|
211
249
|
|
|
212
250
|
### Tuning for low-context llama.cpp
|
|
213
251
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
threshold maps predictably onto what the UI shows you.
|
|
252
|
+
Keep `autoThresholdTokens` comfortably **below** the served context: the live prompt stays
|
|
253
|
+
small and latency flat, while the agent keeps deep memory through the compressed head.
|
|
254
|
+
Pressure is measured in *projected* tokens (provider-anchored), so the threshold maps
|
|
255
|
+
predictably onto the UI figure.
|
|
219
256
|
|
|
220
257
|
---
|
|
221
258
|
|
|
222
259
|
## Behavior notes
|
|
223
260
|
|
|
224
|
-
- **Runtime dependency:** the `compaction` service (preset plane
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
- **Optional dependencies:** `settings` / `tokenMeter` / `commands` / `llm` / `agents` are
|
|
228
|
-
via `ctx.get(...)` with guards — a missing one degrades gracefully
|
|
229
|
-
- **Per-request settings read:** parameters are read
|
|
230
|
-
the next request without a restart.
|
|
231
|
-
- **Signals:**
|
|
261
|
+
- **Runtime dependency:** the `compaction` service (preset plane
|
|
262
|
+
`agent-presets:compaction-basic`), read live via `ctx.get('compaction')`; unreachable →
|
|
263
|
+
the builtin engine takes over (or the request proceeds).
|
|
264
|
+
- **Optional dependencies:** `settings` / `tokenMeter` / `commands` / `llm` / `agents` are
|
|
265
|
+
read via `ctx.get(...)` with guards — a missing one degrades gracefully.
|
|
266
|
+
- **Per-request settings read:** parameters are read every model request, so edits take
|
|
267
|
+
effect on the next request without a restart.
|
|
268
|
+
- **Signals:** `agent/*` Waterfalls forward the current turn's signal; the `session/flush`
|
|
232
269
|
checkpoint and the `agent/status` idle listener each mint a fresh `AbortController`.
|
|
233
|
-
- **Persistence:**
|
|
270
|
+
- **Persistence:** durable output is the `compaction/*` bracket events + a
|
|
234
271
|
`surfaceOp:replace` `user/message` checkpoint, replay-safe across builds.
|
|
235
|
-
- **Client half:** `web/client.js` adds
|
|
236
|
-
|
|
272
|
+
- **Client half:** `web/client.js` adds the settings section "Force Compact" (localized
|
|
273
|
+
labels), live-editable without restart (uSES-safe mirror).
|
|
237
274
|
- **One intentional timer:** the 3 s `publishDone` fallback (presentation-only, documented
|
|
238
275
|
deviation). Otherwise the plugin is pure listeners + a process-local `Map` force flag.
|
|
239
276
|
|
|
240
277
|
---
|
|
241
278
|
|
|
279
|
+
## Screenshots
|
|
280
|
+
|
|
281
|
+

|
|
282
|
+
|
|
283
|
+
*Settings page — the **Force Compact** section; all nine fields above are editable live
|
|
284
|
+
without a restart.*
|
|
285
|
+
|
|
286
|
+

|
|
287
|
+
|
|
288
|
+
*Conversation page — the LiveUI signal paints three states (red: compressing / green: done /
|
|
289
|
+
blue: working); the green banner fades after about 3 s back to a random working line.*
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
242
293
|
## License
|
|
243
294
|
|
|
244
295
|
MIT (see LICENSE).
|
package/index.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-force-compact — a DSH Cordis function plugin.
|
|
3
3
|
*
|
|
4
|
-
* Hooks the core model-request seam so
|
|
5
|
-
*
|
|
4
|
+
* Hooks the core model-request seam so the "强制压缩配置" (force-compact)
|
|
5
|
+
* settings are read on **every model request**:
|
|
6
6
|
*
|
|
7
7
|
* - **`agent/request`** (a Waterfall around the frozen call configuration) —
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* a deliberate **pass-through**: business model requests ride the machine's
|
|
9
|
+
* `LlmCallConfig` unchanged. The `disableThinking` setting does NOT blanket
|
|
10
|
+
* business calls (2026-08 semantics revision) — it scopes STRICTLY to the
|
|
11
|
+
* plugin's own compaction summarization call (`engine/summarizer.js` reads
|
|
12
|
+
* `settings.disableThinking` and stamps `reasoningEffort: 'off'` on its
|
|
13
|
+
* `ctx.llm.stream` options; `engine/builtin.js` routes the flag through the
|
|
14
|
+
* `extra` argument). Reading the settings here on every request means a
|
|
15
|
+
* `settings.yaml` edit is picked up on the next request regardless of scope.
|
|
12
16
|
* - **`agent/pre-step`** (a Waterfall before each model step) — reads the
|
|
13
17
|
* session's total context tokens; when they reach the `autoThresholdTokens`
|
|
14
18
|
* threshold the proposed step is rejected (the model request is NOT made)
|
|
@@ -29,10 +33,10 @@
|
|
|
29
33
|
* - `core/log.js` — the debug-log sink (routes `[force-compact]` lines to `logFile`).
|
|
30
34
|
* - `engine/region.js` — the plugin's own head-anchored region selection.
|
|
31
35
|
* - `engine/summarizer.js` — the plugin's own one-shot LLM summarizer (preview + shrink gate).
|
|
32
|
-
* - `engine/builtin.js` — the self-contained compaction engine (`
|
|
36
|
+
* - `engine/builtin.js` — the self-contained compaction engine (official-named `compaction/*` transactions).
|
|
33
37
|
* - `engine/checkpoint.js` — the `session/flush` checkpoint orchestrator: region → delegate to a backend.
|
|
34
38
|
* - `engine/backend.js` — the unified backend facade (official-service-first, builtin-fallback).
|
|
35
|
-
* - `hooks/guard.js` — the per-model-request guard: threshold gate + forced compaction +
|
|
39
|
+
* - `hooks/guard.js` — the per-model-request guard: threshold gate + forced compaction (+ legacy `thinkingDisabled` predicate).
|
|
36
40
|
* - `hooks/command.js` — the `/force-compact` slash command (idle → compact now; busy → queue a force flag).
|
|
37
41
|
* - `hooks/idle.js` — the turn-end (agent `idle`) forced compaction.
|
|
38
42
|
* - `web/client.js` — the browser half: the Force-Compact settings.section UI.
|
|
@@ -43,7 +47,12 @@
|
|
|
43
47
|
import { compactSession } from './src/engine/checkpoint.js'
|
|
44
48
|
import { registerNamespace, readRawSetting } from './src/core/settings.js'
|
|
45
49
|
import { ensureDebugLogger } from './src/core/log.js'
|
|
50
|
+
// `thinkingDisabled` is imported solely to keep the `guard.thinkingDisabled`
|
|
51
|
+
// helper reachable from the plugin root for consumers who DO want the blanket
|
|
52
|
+
// "off everywhere" predicate; the active `agent/request` hot path no longer
|
|
53
|
+
// consumes it (see the pass-through comment on `__agentRequestListenerBody`).
|
|
46
54
|
import { forceCompactIfNeeded, thinkingDisabled } from './src/hooks/guard.js'
|
|
55
|
+
void thinkingDisabled
|
|
47
56
|
import { registerCommand } from './src/hooks/command.js'
|
|
48
57
|
import { handleAgentStatus } from './src/hooks/idle.js'
|
|
49
58
|
import { registerLlmStreamHook } from './src/hooks/wire-rewrite.js'
|
|
@@ -348,15 +357,16 @@ const __applyInner = (ctx) => {
|
|
|
348
357
|
registerLlmStreamHook(ctx)
|
|
349
358
|
}
|
|
350
359
|
|
|
351
|
-
// Hook the core model request:
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
// `agent/request` is a Waterfall — `await
|
|
355
|
-
// machine would use;
|
|
360
|
+
// Hook the core model request: a DELIBERATE PASS-THROUGH (2026-08 semantics
|
|
361
|
+
// revision) — business model requests carry the machine's own reasoning
|
|
362
|
+
// effort UNCHANGED; `disableThinking` scopes strictly to this plugin's
|
|
363
|
+
// compaction summarization call. `agent/request` is a Waterfall — `await
|
|
364
|
+
// next()` yields the config the machine would use; we forward it as-is. The
|
|
365
|
+
// listener stays registered because the seam MUST `next()` and the lazy
|
|
366
|
+
// install hooks below ride its first activation.
|
|
356
367
|
guard('agent/request listener', () => ctx.on('agent/request', async (payload, next) => {
|
|
357
|
-
// SAFETY ENVELOPE: this is a PER-MODEL-REQUEST seam — an anomaly
|
|
358
|
-
//
|
|
359
|
-
// traps on spread) must degrade to PASSING THROUGH the original config so
|
|
368
|
+
// SAFETY ENVELOPE: this is a PER-MODEL-REQUEST seam — an anomaly during
|
|
369
|
+
// the lazy installs must degrade to PASSING THROUGH the original config so
|
|
360
370
|
// the request proceeds normally, never crashing the request chain.
|
|
361
371
|
try {
|
|
362
372
|
maybeInstallDebugSink()
|
|
@@ -374,24 +384,21 @@ const __applyInner = (ctx) => {
|
|
|
374
384
|
/** Body of the `agent/request` listener; wrapped by its safe envelope above. */
|
|
375
385
|
async function __agentRequestListenerBody(ctx, payload, next) {
|
|
376
386
|
const config = await next()
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
//
|
|
384
|
-
//
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
if (!isObj) return config
|
|
393
|
-
ctx.logger.debug(`[force-compact] agent/request: applying reasoningEffort=off (disableThinking=true) — original=${currentEffort ?? '(unset)'}`)
|
|
394
|
-
return { ...config, reasoningEffort: 'off' }
|
|
387
|
+
void payload
|
|
388
|
+
void ctx
|
|
389
|
+
// DELIBERATE PASS-THROUGH (2026-08 semantics revision): the `disableThinking`
|
|
390
|
+
// setting now scopes STRICTLY to THIS PLUGIN'S OWN compaction summarization
|
|
391
|
+
// call (enforced at `summarizer.js`, where `extra.reasoningEffort` is sourced
|
|
392
|
+
// from `settings.disableThinking`). Business model requests ride the machine's
|
|
393
|
+
// config UNCHANGED — whatever the deployment's request header carried is
|
|
394
|
+
// honored, and a `settings.yaml` edit is picked up on the next request because
|
|
395
|
+
// this listener still runs every request. `agent/request` sits on the agent
|
|
396
|
+
// LOOP seam: business conversation steps only — the plugin's (and the
|
|
397
|
+
// official engine's) summarization calls never traverse it, so scoping the
|
|
398
|
+
// flag here would have stamped EVERY business call while reaching neither
|
|
399
|
+
// summarizer. The seam itself MUST still `await next()` (documented
|
|
400
|
+
// contract), hence this listener remains registered.
|
|
401
|
+
return config
|
|
395
402
|
}
|
|
396
403
|
|
|
397
404
|
// Before each model step, run a forced/threshold-triggered compaction as a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@falling-ts/dsh-force-compact",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
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",
|
package/src/engine/builtin.js
CHANGED
|
@@ -727,6 +727,12 @@ async function runTransaction(ctx, agent, session, region, signal, settings, sou
|
|
|
727
727
|
let summarizationMaxTokens
|
|
728
728
|
try {
|
|
729
729
|
const extra = { reasoningEffort: settings.disableThinking ? 'off' : undefined }
|
|
730
|
+
// AUDIT LOG (2026-08 addition): state the thinking-scoping DECISION at the
|
|
731
|
+
// moment the `disableThinking` setting is READ and routed into `extra`.
|
|
732
|
+
// One line per summarization attempt (NOT per model request — this is the
|
|
733
|
+
// compaction path only), so the log doubles as the durable answer to
|
|
734
|
+
// "did this compaction carry thinking-off?" without needing the wire trace.
|
|
735
|
+
info(ctx, `${session.id}: compaction thinking-policy — settings.disableThinking=${settings.disableThinking} → extra.reasoningEffort=${settings.disableThinking ? "'off' (this summarization call carries thinking-OFF)" : '(unset — summarization call RIDES MACHINE DEFAULT, no thinking override)'}`)
|
|
730
736
|
if (Number.isFinite(settings.maxSummaryTokens) && settings.maxSummaryTokens > 0) {
|
|
731
737
|
extra.maxTokens = settings.maxSummaryTokens
|
|
732
738
|
}
|
package/src/engine/summarizer.js
CHANGED
|
@@ -242,6 +242,57 @@ async function __summarizeBody(ctx, config, agent, input, signal, extra) {
|
|
|
242
242
|
// purpose-specific generation policy). The agent's free-form purpose string
|
|
243
243
|
// is NOT a valid `GenerateOptions.purpose` value.
|
|
244
244
|
options.purpose = 'compaction'
|
|
245
|
+
// LLAMA.CPP COMPATIBILITY WIRE FIELD (2026-08 addition): when the caller
|
|
246
|
+
// requested "thinking off" for this compaction (extra.reasoningEffort === 'off',
|
|
247
|
+
// which `engine/builtin.js` sets whenever `settings.disableThinking` is true),
|
|
248
|
+
// ALSO emit the OPENAI-COMPATIBLE top-level wire field
|
|
249
|
+
// `reasoning_effort: 'none'` alongside the camelCase `reasoningEffort`
|
|
250
|
+
// (line 225 above). The DeepSeek adapter's wire mapping translates the
|
|
251
|
+
// camelCase into `thinking: { type: 'disabled' }`, which the real DeepSeek
|
|
252
|
+
// API honors but llama.cpp's OAI parsing path (tools/server/server-common.
|
|
253
|
+
// cpp) ignores (top-level `thinking` is not in its schema — it is forwarded
|
|
254
|
+
// opaquely into `llama_params` and silently dropped). llama.cpp DOES natively
|
|
255
|
+
// parse a TOP-LEVEL `reasoning_effort: "none"` keyword into
|
|
256
|
+
// `inputs.enable_thinking = false` UNCONDITIONALLY (server-common.cpp:1295-
|
|
257
|
+
// 1304), independent of jinja-template capability. By emitting BOTH fields
|
|
258
|
+
// on the same options object we cover both endpoints simultaneously:
|
|
259
|
+
// - Real DeepSeek endpoint: reads `reasoningEffort` → emits
|
|
260
|
+
// `thinking: { type: 'disabled' }`; ignores the unknown
|
|
261
|
+
// `reasoning_effort` top-level key (silent no-op, no 400).
|
|
262
|
+
// - llama.cpp / OpenAI-compatible endpoint: reads the top-level
|
|
263
|
+
// `reasoning_effort: "none"` → enables the native
|
|
264
|
+
// `enable_thinking = false` path; the adapter's `thinking` field
|
|
265
|
+
// (still present in the body) is tolerated-but-ignored there.
|
|
266
|
+
// Because `builtin.js` gates `extra.reasoningEffort` on
|
|
267
|
+
// `settings.disableThinking`, this compatibility field rides the EXACT
|
|
268
|
+
// same scoping rule as the primary one — only emitted on compaction calls
|
|
269
|
+
// where the user has turned thinking OFF; business-conversation and other
|
|
270
|
+
// LLM calls never reach this code path and are unaffected.
|
|
271
|
+
if (extra !== undefined && extra.reasoningEffort === 'off') {
|
|
272
|
+
options.reasoning_effort = 'none'
|
|
273
|
+
}
|
|
274
|
+
// AUDIT LOG (2026-08 addition): the LLAMA.CPP-COMPATIBILITY stamp site. One
|
|
275
|
+
// line per summarization attempt recording BOTH wire fields that leave the
|
|
276
|
+
// options object and the resolved provider/model they head to — the durable
|
|
277
|
+
// answer to "did this compaction's thinking-off actually land on the wire?".
|
|
278
|
+
// Defensive envelope: a missing/non-function `ctx.logger.debug` degrades to
|
|
279
|
+
// silence (never escapes `summarize`). NOTE (2026-08 guard fix): `ctx.logger`
|
|
280
|
+
// is a CALLABLE LoggerService façade (`createCallable` — `typeof 'function'`,
|
|
281
|
+
// not 'object'), so the original `typeof ctx.logger === 'object'` guard was
|
|
282
|
+
// ALWAYS false and this line never fired. The guard below mirrors
|
|
283
|
+
// engine/builtin.js's working `info()` envelope instead: presence check +
|
|
284
|
+
// duck-typed `.debug`, wrapped in try/catch.
|
|
285
|
+
{
|
|
286
|
+
const _auditPrimary = options.reasoningEffort === undefined ? '(absent — machine default)' : `'${options.reasoningEffort}'`
|
|
287
|
+
const _auditCompat = options.reasoning_effort === undefined ? '(absent — thinking rides machine default)' : `"${options.reasoning_effort}" (llama.cpp-native wire field)`
|
|
288
|
+
const _auditWho = session !== undefined && session !== null && typeof session.id === 'string' ? session.id + ': ' : ''
|
|
289
|
+
const _auditLogger = (ctx === undefined || ctx === null) ? null : ctx.logger
|
|
290
|
+
try {
|
|
291
|
+
if (_auditLogger !== null && _auditLogger !== undefined && typeof _auditLogger.debug === 'function') {
|
|
292
|
+
_auditLogger.debug('[force-compact] ' + _auditWho + 'summarization wire-fields → ' + target.provider + '/' + target.model + ': reasoningEffort=' + _auditPrimary + ' + reasoning_effort=' + _auditCompat)
|
|
293
|
+
}
|
|
294
|
+
} catch {}
|
|
295
|
+
}
|
|
245
296
|
|
|
246
297
|
// Assemble ALL chunk kinds (text + reasoning + images). Reasoning deltas are
|
|
247
298
|
// dropped later by `extractTextOnly`; a terminal finish decides whether the
|
package/src/hooks/guard.js
CHANGED
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
* before a model request is made**:
|
|
8
8
|
*
|
|
9
9
|
* - **`agent/request`** (a Waterfall around the frozen call configuration) —
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* a deliberate **pass-through** (2026-08 semantics revision): the returned
|
|
11
|
+
* `LlmCallConfig` rides UNCHANGED. `disableThinking` now scopes strictly to
|
|
12
|
+
* this plugin's own compaction summarization call (enforced inside
|
|
13
|
+
* `engine/builtin.js` → `engine/summarizer.js`); all other model requests
|
|
14
|
+
* retain the machine's own reasoning-effort configuration.
|
|
14
15
|
* - **`agent/pre-step`** (a Waterfall before each model step) — reads the
|
|
15
16
|
* session's **projected context tokens** through the official
|
|
16
17
|
* `contextPressure` projection (`projectedTokens` — the exact figure the
|
|
@@ -640,11 +641,21 @@ async function __forceCompactIfNeededBody(ctx, agent, signal, mode) {
|
|
|
640
641
|
}
|
|
641
642
|
|
|
642
643
|
/**
|
|
643
|
-
* Whether a model request
|
|
644
|
+
* Whether a model request SHOULD be sent with thinking/reasoning disabled.
|
|
644
645
|
*
|
|
645
|
-
*
|
|
646
|
-
*
|
|
647
|
-
*
|
|
646
|
+
* LEGACY PREDICATE (2026-08 semantics revision): the active `agent/request`
|
|
647
|
+
* hot path no longer calls this helper. `disableThinking` now scopes STRICTLY
|
|
648
|
+
* to THIS PLUGIN'S OWN compaction summarization call — enforced inside
|
|
649
|
+
* `src/engine/builtin.js` (which sources `extra.reasoningEffort` from
|
|
650
|
+
* `settings.disableThinking` and passes it to `src/engine/summarizer.js`,
|
|
651
|
+
* whose `options.reasoningEffort` stamps the `ctx.llm.stream` request). All
|
|
652
|
+
* OTHER model requests (business conversation, sub-agents, tool-driven,
|
|
653
|
+
* other plugins) ride the machine's config UNCHANGED.
|
|
654
|
+
*
|
|
655
|
+
* Kept exported so a FUTURE caller who genuinely wants the blanket
|
|
656
|
+
* "off-everywhere" semantics (or the legacy dual-layer insurance described
|
|
657
|
+
* in the stale docs) can consume the same setting through the same
|
|
658
|
+
* containment envelope without duplicating the read-settings logic.
|
|
648
659
|
*
|
|
649
660
|
* @param {import('@deepseek-ai/cordis').Context} ctx
|
|
650
661
|
* @returns {Promise<boolean>}
|