@falling-ts/dsh-force-compact 0.2.2 → 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 +169 -110
- package/README.md +149 -85
- package/assets/live-conversation.png +0 -0
- package/assets/settings-panel.png +0 -0
- package/index.js +41 -34
- package/package.json +2 -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
|
@@ -1,117 +1,127 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dsh-force-compact's
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* dsh-force-compact's Live-UI watermark side-channel on the `llm/stream`
|
|
3
|
+
* waterfall seam — KICKS OFF a fresh random "working" one-liner on every LLM
|
|
4
|
+
* call START so the Live UI (browser) repaints the `TurnStatus` node with a
|
|
5
|
+
* new `liveUi.working` pair. Purely a PRESENTATION-LAYER concern (a settings-
|
|
6
|
+
* write on the `liveUi` field, mirrored live to the browser via the existing
|
|
7
|
+
* settings-sync channel). Performs NO wire modification whatsoever.
|
|
5
8
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* OpenAI-compatible endpoint (:8080, `Qwen3.8-27B-NVFP4-MTP-LOW.gguf`), the
|
|
14
|
-
* top-level `thinking` key is NOT in the llama.cpp request schema — it is
|
|
15
|
-
* forwarded opaquely into `llama_params` and IGNORED
|
|
16
|
-
* (verified against `D:\AI\llama.cpp\tools\server`: `thinking` appears
|
|
17
|
-
* nowhere in `server-schema.cpp`, and the OAI parsing path never reads a
|
|
18
|
-
* top-level `thinking` key). Net effect of the request-seam path ALONE:
|
|
19
|
-
* `disableThinking:true` SILENTLY FAILS to turn off thinking on llama.cpp —
|
|
20
|
-
* the model thinks anyway, with no error surfacing.
|
|
9
|
+
* Why NOT at the `llm/stream` seam for wire-fields (historical note, 2026-08)
|
|
10
|
+
* ----------------------------------------------------------------------------
|
|
11
|
+
* An EARLIER draft of this module attempted to APPEND the llama.cpp-native
|
|
12
|
+
* wire field `reasoning_effort: "none"` to outgoing LLM calls at this same
|
|
13
|
+
* `llm/stream` seam. That approach is PROVABLY INEFFECTIVE HERE for two
|
|
14
|
+
* independent structural reasons (both verified empirically against the
|
|
15
|
+
* harness dispatch code and Cordis waterfall semantics):
|
|
21
16
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* (
|
|
29
|
-
*
|
|
17
|
+
* 1. WATERFALL IS A LINEAR CHAIN THAT DISCARDS INTERMEDIATE RETURNS.
|
|
18
|
+
* `ctx.waterfall(...)` (vendor/cordis/src/events.ts:234-243) walks its
|
|
19
|
+
* listener array sequentially, always passing the SAME frozen seed
|
|
20
|
+
* `args` to each successive layer, and returns the OUTERMOST layer's
|
|
21
|
+
* value as the final stream. Intermediate layers' return values are
|
|
22
|
+
* DROPPED — they influence nothing downstream. Our plugin registers
|
|
23
|
+
* LAST (lazy-install, default `push` order), so our return reaches
|
|
24
|
+
* nobody; the innermost thunk receives the ORIGINAL seed reference
|
|
25
|
+
* regardless of what we return.
|
|
30
26
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
27
|
+
* 2. IN-PLACE ASSIGNMENT TO THE FROZEN SEED CRASHES. The seed is a deep-
|
|
28
|
+
* frozen (non-extensible) `GenerateOptions`; `seed.reasoning_effort =
|
|
29
|
+
* 'none'` throws `Cannot add property …, object is not extensible` at
|
|
30
|
+
* the instant a real LLM call fires, propagating OUT OF the listener
|
|
31
|
+
* into the host process and taking the entire `dsh web` instance down.
|
|
32
|
+
*
|
|
33
|
+
* Net effect: any injection-at-the-waterfall design either CRASHES (wall 1)
|
|
34
|
+
* or SILENTLY NO-OPS (wall 2). Both were observed in live testing on a
|
|
35
|
+
* running 3180 dev instance; that is the documented reason the module was
|
|
36
|
+
* reduced to a pure passthrough.
|
|
37
|
+
*
|
|
38
|
+
* Where the wire-append ACTUALLY lives now (since 2026-08)
|
|
39
|
+
* ----------------------------------------------------------------------
|
|
40
|
+
* The correct single-line fix landed in `src/engine/summarizer.js` IMMEDIATELY
|
|
41
|
+
* BEFORE the `llm.stream(options)` call (search for the comment block titled
|
|
42
|
+
* "LLAMA.CPP COMPATIBILITY WIRE FIELD"): when `extra.reasoningEffort === 'off'`
|
|
43
|
+
* (which `engine/builtin.js` stamps whenever `settings.disableThinking` is
|
|
44
|
+
* true), the summarizer sets `options.reasoning_effort = 'none'` ALONGSIDE the
|
|
45
|
+
* existing camelCase `options.reasoningEffort = 'off'` field (which the
|
|
46
|
+
* DeepSeek adapter serializes to `thinking:{type:'disabled'}`). Emitting BOTH
|
|
47
|
+
* fields covers BOTH endpoints simultaneously:
|
|
40
48
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* clone and returning it (or re-dispatching `this.stream(clone)`) never
|
|
51
|
-
* reaches the serializer, and re-dispatch additionally fails because the
|
|
52
|
-
* listener's `this` is not reliably the LlmRuntime here (`Cannot read
|
|
53
|
-
* properties of undefined (reading 'stream')`).
|
|
49
|
+
* • Real DeepSeek endpoint: reads `reasoningEffort` → emits
|
|
50
|
+
* `thinking: { type: 'disabled' }`; ignores the unknown snake_case
|
|
51
|
+
* `reasoning_effort` top-level key (silent no-op, no 400).
|
|
52
|
+
* • llama.cpp / OpenAI-compatible endpoint: reads the top-level
|
|
53
|
+
* `reasoning_effort: "none"` → parses natively into
|
|
54
|
+
* `inputs.enable_thinking = false` UNCONDITIONALLY
|
|
55
|
+
* (`D:\AI\llama.cpp\tools\server\server-common.cpp:1295-1304`); the
|
|
56
|
+
* adapter's `thinking` field (still present in the body) is tolerated-
|
|
57
|
+
* but-ignored there.
|
|
54
58
|
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* it forwards `next()`'s result (the real stream) untouched and performs NO
|
|
61
|
-
* mutation — incapable of breaking any business-path model call.
|
|
59
|
+
* Because `builtin.js` gates `extra.reasoningEffort` on
|
|
60
|
+
* `settings.disableThinking`, the wire-field rides the EXACT same scoping
|
|
61
|
+
* rule as the primary one: only emitted on COMPRACTION calls where the user
|
|
62
|
+
* has turned thinking OFF. Business-conversation requests and every other LLM
|
|
63
|
+
* call never reach `summarize()` at all, so they are UNAFFECTED.
|
|
62
64
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
65
|
+
* What THIS FILE STILL DOES (Live-UI watermark)
|
|
66
|
+
* ---------------------------------------------
|
|
67
|
+
* Independent of the wire question above, the listener KICKS OFF a fire-
|
|
68
|
+
* and-forget `publishRandomWorking(ctx)` call BEFORE the synchronization
|
|
69
|
+
* point so the Live UI paints a fresh random "working" one-liner on every LLM
|
|
70
|
+
* call start. This is purely a presentation-layer concern (a settings-write
|
|
71
|
+
* on the `liveUi` field, mirrored live to the browser so it can repaint the
|
|
72
|
+
* `TurnStatus` node) and MUST NOT BLOCK the return path. `publishRandomWorking`
|
|
73
|
+
* swallows ALL of its own rejections internally, so the fire-and-forget form
|
|
74
|
+
* leaks no unhandled rejection and never disturbs the stream.
|
|
75
|
+
*
|
|
76
|
+
* Contract guarantees
|
|
77
|
+
* --------------------
|
|
78
|
+
* • ALWAYS calls `next()` (skipping it would stall the waterfall chain).
|
|
79
|
+
* • NEVER mutates the (deep-frozen) seed, so it cannot raise
|
|
80
|
+
* `object is not extensible`.
|
|
81
|
+
* • NEVER reshapes / spreads the (stream) return value, so it cannot break
|
|
82
|
+
* the consumer's `for await`.
|
|
83
|
+
* • Is DECLARED NON-ASYNC: an `async` listener would wrap `next()`'s stream
|
|
84
|
+
* return in a Promise, and the waterfall dispatcher's downstream
|
|
85
|
+
* `yield* <promise>` would throw
|
|
86
|
+
* `yield* (intermediate value)… is not async iterable` on every call.
|
|
71
87
|
*
|
|
72
88
|
* @module @falling-ts/dsh-force-compact/wire-rewrite
|
|
73
89
|
*/
|
|
74
90
|
|
|
75
91
|
/**
|
|
76
|
-
* Register the `llm/stream` Waterfall listener. As of the
|
|
77
|
-
*
|
|
78
|
-
* forwards `next()`'s result (the async-iterable chunk stream)
|
|
79
|
-
* performs NO mutation.
|
|
80
|
-
*
|
|
81
|
-
*
|
|
92
|
+
* Register the `llm/stream` Waterfall listener. As of the historical-note
|
|
93
|
+
* section above, the listener is a DELIBERATE pure passthrough w.r.t. WIRE
|
|
94
|
+
* SEMANTICS: it forwards `next()`'s result (the async-iterable chunk stream)
|
|
95
|
+
* untouched and performs NO mutation. Its sole remaining duty is the Live-UI
|
|
96
|
+
* watermark side-channel (see the module header). See
|
|
97
|
+
* `src/engine/summarizer.js` for where the actual `reasoning_effort` wire
|
|
98
|
+
* field is now injected (at the options-construction site, NOT at this
|
|
99
|
+
* waterfall seam).
|
|
82
100
|
*
|
|
83
101
|
* Idempotent within one plugin lifetime (a process-local latch prevents
|
|
84
102
|
* double-registration across multiple `apply` invocations in tests or HMR).
|
|
85
103
|
*
|
|
86
104
|
* Contract guarantees:
|
|
87
|
-
* Contract guarantees:
|
|
88
105
|
* • ALWAYS calls `next()` (skipping it would stall the waterfall chain).
|
|
89
106
|
* • NEVER mutates the (deep-frozen) seed, so it cannot raise
|
|
90
107
|
* `object is not extensible`.
|
|
91
|
-
* • NEVER reshapes/spreads the (stream) return value, so it cannot break
|
|
92
|
-
* consumer's `for await`.
|
|
93
|
-
* • Emits at most ONE debug line (the first-of-lifetime ui-signal marker;
|
|
94
|
-
* silent thereafter).
|
|
108
|
+
* • NEVER reshapes / spreads the (stream) return value, so it cannot break
|
|
109
|
+
* the consumer's `for await`.
|
|
95
110
|
* • FIRES THE LIVE UI STATUS SIDE-CHANNEL (`core/ui-signal.js`) on every
|
|
96
111
|
* invocation — the "each LLM call start = fresh random working pair"
|
|
97
|
-
* watermark. Publication is KICKED OFF FIRE-AND-FORGET (a plain
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* untouched per the
|
|
104
|
-
* corrupt the stream.
|
|
105
|
-
* • The listener is DECLARED NON-ASYNC: an `async` listener would wrap
|
|
106
|
-
* `next()`'s stream return in a Promise, and the waterfall dispatcher's
|
|
107
|
-
* downstream `yield* <promise>` would throw
|
|
108
|
-
* `yield* (intermediate value)… is not async iterable` on every call.
|
|
112
|
+
* watermark. Publication is KICKED OFF FIRE-AND-FORGET (a plain non-
|
|
113
|
+
* awaited `publishRandomWorking(ctx)` call placed BEFORE the synchronous
|
|
114
|
+
* `return next()`) so the listener itself STAYS SYNC and can directly hand
|
|
115
|
+
* back the real stream; `publishRandomWorking` swallows all of its own
|
|
116
|
+
* rejections internally, so the fire-and-forget form leaks no unhandled
|
|
117
|
+
* rejection. It never touches `payload` (the deep-frozen seed — left
|
|
118
|
+
* untouched per the historical-note section above), so it can never stall
|
|
119
|
+
* or corrupt the stream.
|
|
109
120
|
*
|
|
110
121
|
* @param {import('@deepseek-ai/cordis').Context} ctx
|
|
111
122
|
* @returns {boolean} whether this call actually performed the (once-only)
|
|
112
|
-
* registration. `false` indicates it was a no-op re-entry (already
|
|
113
|
-
*
|
|
114
|
-
* will retry).
|
|
123
|
+
* registration. `false` indicates it was a no-op re-entry (already installed)
|
|
124
|
+
* or a registration failure (not installed; a later re-entry will retry).
|
|
115
125
|
*/
|
|
116
126
|
|
|
117
127
|
import { publishRandomWorking } from '../core/ui-signal.js'
|
|
@@ -131,10 +141,10 @@ export function registerLlmStreamHook(ctx) {
|
|
|
131
141
|
// internally (guaranteed side-effect-free w.r.t. the waterfall), so
|
|
132
142
|
// kicking it off without awaiting cannot leak an unhandled rejection.
|
|
133
143
|
// `payload` is the deep-frozen GenerateOptions seed — NEVER mutated (see
|
|
134
|
-
// the
|
|
135
|
-
// none of that (pure settings-write on the `liveUi` field,
|
|
136
|
-
// to the browser so it can repaint the `TurnStatus` node).
|
|
137
|
-
// documents the deliberate non-use.
|
|
144
|
+
// the historical-note section in the module header); the publication
|
|
145
|
+
// touches none of that (pure settings-write on the `liveUi` field,
|
|
146
|
+
// mirrored live to the browser so it can repaint the `TurnStatus` node).
|
|
147
|
+
// `void payload` documents the deliberate non-use.
|
|
138
148
|
ctx.on('llm/stream', (payload, next) => {
|
|
139
149
|
void payload
|
|
140
150
|
publishRandomWorking(ctx) // fire-and-forget: sync kick-off, async settle
|
|
@@ -142,10 +152,8 @@ export function registerLlmStreamHook(ctx) {
|
|
|
142
152
|
})
|
|
143
153
|
installed = true
|
|
144
154
|
return true
|
|
145
|
-
} catch {
|
|
146
|
-
|
|
147
|
-
// fatal to NOTHING — the plugin simply never installs this hook. We DO
|
|
148
|
-
// NOT mark `installed`, so a later re-entry retries.
|
|
155
|
+
} catch (err) {
|
|
156
|
+
ctx.logger?.warn('[force-compact] wire-rewrite install failed:', err?.message ?? err)
|
|
149
157
|
return false
|
|
150
158
|
}
|
|
151
159
|
}
|