@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.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 *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.
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 itselfa 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
- 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.**
17
-
18
- - **Self-hosted inference.** Point the agent at a local OpenAI-compatible llama.cpp server
19
- running `Qwen3.8‑27B` (GGUF / NVFP4 / MTP variants all work through the standard DeepSeek
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.
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 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. |
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
- You never toggle — official wins when reachable, builtin takes over otherwise.
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, 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.
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 live-UI signal fires.
52
+ same point where the LiveUI signal fires.
57
53
 
58
- ### Provider-anchored decisions
59
-
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).
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 instead of drifting upward.
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
- ### Backend-agnostic thinking control
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
- `disableThinking` is enforced at **two complementary seams**:
94
+ #### Observability: per-attempt audit lines
73
95
 
74
- 1. **Request seam** `reasoningEffort:'off'` the DeepSeek adapter serializes
75
- `thinking:{type:'disabled'}` (real DeepSeek APIs honor it).
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
- Result: thinking is genuinely off on **any** backend, with no target-sniffing heuristic to miss
82
- a route.
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
- ### Live-UI status
114
+ ### LiveUI status
85
115
 
86
- A tiny host→client messenger (a `liveUi` settings field mirrored live to the browser) paints a
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
- - **🟥 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 ("正在缝合上下文…", "正在憋大招…").
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
- disableThinking? -> { ...config, reasoningEffort: "off" }
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 model request proceed
137
+ no -> next() # let the request proceed
107
138
  yes -> compactRegion(head-before-retainLatestTokens, signal)
108
- return { kind: "reject" } # no model request this step
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` — per-request guard: thinking-off + threshold gate + forced flag.
122
- - `src/hooks/command.js` the `/force-compact` command.
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` wire patch appending `reasoning_effort:"none"`.
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 (fully aligned with official
127
- `compaction-basic`: target resolution, prefix-cache alignment, `purpose:'compaction'` tag,
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` reading.
132
- - `src/core/ui-signal.js` — the live-UI messenger.
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 bundle (recommended):
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
- Plugin loaded `~/.dsh/logs/dsh-force-compact.log` gains:
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` | 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. |
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
- When the `settings` service is absent, the plugin falls back to the same defaults and still
197
- compacts — the namespace is optional, never a hard dependency.
198
-
199
- ---
200
-
201
- ## Screenshots
202
-
203
- ![Settings panel — "Force Compaction / 强制压缩" section with all five knobs live-editable](assets/settings-panel.png)
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
- Serve `Qwen3.8‑27B` with a comfortable-but-modest context and let the plugin decide the
215
- effective window: keep `autoThresholdTokens` comfortably **below** the served context so the
216
- live prompt stays small and latency flat, while the agent retains deep memory through the
217
- compressed head. Pressure is measured in *projected* tokens (provider-anchored), so the
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 `agent-presets:compaction-basic`).
225
- Read live via `ctx.get('compaction')`; when unreachable the forced-compaction path falls
226
- through and lets the request proceed.
227
- - **Optional dependencies:** `settings` / `tokenMeter` / `commands` / `llm` / `agents` are read
228
- via `ctx.get(...)` with guards — a missing one degrades gracefully rather than crashing.
229
- - **Per-request settings read:** parameters are read per model request, so edits take effect on
230
- the next request without a restart.
231
- - **Signals:** the `agent/*` Waterfalls forward the current turn's signal; the `session/flush`
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:** the durable output is the `compaction/*` bracket events + a
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 a Settings section "强制压缩 / Force Compact" for
236
- editing these values live (uSES-safe mirror, no timers/state).
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
+ ![Settings panel — Force Compact section, all knobs live-editable](assets/settings-panel.png)
282
+
283
+ *Settings page — the **Force Compact** section; all nine fields above are editable live
284
+ without a restart.*
285
+
286
+ ![Conversation page — red "compressing" badge pinned beside an in-flight turn](assets/live-conversation.png)
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 that, on **every model request**, the
5
- * "强制压缩配置" (force-compact) settings are read:
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
- * when the `disableThinking` setting is on, the returned `LlmCallConfig`
9
- * carries `reasoningEffort: 'off'`, which the LLM adapter maps to
10
- * `thinking: { type: 'disabled' }`. Every model request is therefore sent
11
- * with thinking/reasoning disabled.
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 (`fc-compact/*` transactions).
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 + thinking-off.
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: when "disable thinking" is on, every model
352
- // request carries reasoningEffort: 'off'. Reading the settings here (per
353
- // request) means a settings.yaml edit is picked up on the next request.
354
- // `agent/request` is a Waterfall — `await next()` yields the config the
355
- // machine would use; returning a replacement switches it.
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 (a
358
- // non-object `config` seed, a rejecting `thinkingDisabled`, a Proxy that
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
- if (!payload || config === undefined || config === null) return config
378
- if (!(await thinkingDisabled(ctx))) {
379
- // disableThinking=false (setting off): leave the machine's config untouched.
380
- ctx.logger.debug('[force-compact] agent/request: disableThinking=false leaving reasoning effort unchanged')
381
- return config
382
- }
383
- // `config` may be a non-object seed; guard the property reads so a weird shape
384
- // degrades to returning it untouched rather than throwing on `.reasoningEffort`.
385
- const isObj = (config !== null && typeof config === 'object')
386
- const currentEffort = isObj ? config.reasoningEffort : undefined
387
- if (currentEffort === 'off') {
388
- // Already off nothing to switch (still proves the guard is active on this request).
389
- ctx.logger.debug('[force-compact] agent/request: reasoningEffort already off no change')
390
- return config
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",
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",
@@ -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
  }
@@ -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
@@ -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
- * when the `disableThinking` setting is on, the returned `LlmCallConfig`
11
- * carries `reasoningEffort: 'off'`, which the LLM adapter maps to
12
- * `thinking: { type: 'disabled' }`. Every model request in this process is
13
- * therefore sent with thinking/reasoning disabled.
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 should be sent with thinking/reasoning disabled.
644
+ * Whether a model request SHOULD be sent with thinking/reasoning disabled.
644
645
  *
645
- * Called from the `agent/request` Waterfall. When the `disableThinking`
646
- * setting is on (default), the caller sets `reasoningEffort: 'off'` on the
647
- * returned `LlmCallConfig`.
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>}