dsh-compact-relay 0.4.0 → 0.4.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.
@@ -0,0 +1,219 @@
1
+ /**
2
+ * 交接线保命判定 —— 纯函数,零依赖(0.4.2)。
3
+ *
4
+ * 背景(0.4.1 遗留的三个洞,见 docs/0.4.2-implementation-plan.md):
5
+ * 1. 交接提醒线仍是配置值(默认 60%),换到实际触发线更低的模型(如 55%)时,
6
+ * DSH 会先自己压历史,dcr 的交接提醒来不及;
7
+ * 2. 服务端保存校验只看配置阈值(80%),手改设置文件能设到 70%;
8
+ * 3. 旧值不跟随当前模型,除设置页一行 warning 外没有其它可见提示。
9
+ *
10
+ * 0.4.2 规则(范围定稿):
11
+ * 交接提醒线 ≤ 实际触发线 − 5pp(HANDOFF_MARGIN_PP),向下取整到整数百分位。
12
+ * - 实际触发线来自 state.native.ratio(按当前路由模型实时算,见 core/native-line.js);
13
+ * - 实际线算不出时:运行时不告警、不阻断;服务端保存退回配置阈值防呆(0.4.1 行为);
14
+ * - DSH 原生自动压缩关闭时不存在「被抢先压缩」的截止线,不判定;
15
+ * - 只告警 / 拒绝,绝不静默改写用户配置。
16
+ *
17
+ * 本模块只做判定与文案,不做 IO:服务端 `lib/actions/settings.js` 直接 import,
18
+ * 客户端由 `scripts/build-client.mjs` 的 `/* @inline core/handoff-guard.js *\/`
19
+ * 内联进 `lib/client.js`(同一份判定,避免 UI 与后台两套口径)。
20
+ */
21
+
22
+ /** 交接线相对实际触发线至少留 5 个百分点(1M 窗口 ≈ 50K tokens 缓冲)。 */
23
+ export const HANDOFF_MARGIN_PP = 5;
24
+
25
+ /** 实际线算不出、退回配置阈值防呆时的余量(0.4.1 口径:最高 = 配置阈值 −1pp)。 */
26
+ export const FALLBACK_MARGIN_PP = 1;
27
+
28
+ /** cap 的绝对上限(避免算出 96%+ 这种没有意义的上限)。 */
29
+ export const HANDOFF_LIMIT_DEFAULT = 0.95;
30
+
31
+ const EPS = 1e-9;
32
+
33
+ /**
34
+ * 合法窗口比例:必须是 0<r<1 的有限数。
35
+ * @param {unknown} value
36
+ * @returns {number|null}
37
+ */
38
+ function ratioOf(value) {
39
+ return typeof value === 'number' && Number.isFinite(value) && value > 0 && value < 1 ? value : null;
40
+ }
41
+
42
+ /**
43
+ * 比例 → 百分数数字(一位小数,整数不带小数):0.678 → 67.8,0.62 → 62。
44
+ * @param {number|null} ratio
45
+ * @returns {number|null}
46
+ */
47
+ function percentOf(ratio) {
48
+ if (ratio === null) return null;
49
+ const percent = ratio * 100;
50
+ return Math.abs(percent - Math.round(percent)) < 0.05 ? Math.round(percent) : Number(percent.toFixed(1));
51
+ }
52
+
53
+ /**
54
+ * 比例 → 百分数文本(用于文案):0.678 → '67.8',0.62 → '62'。
55
+ * @param {number|null} ratio
56
+ * @returns {string}
57
+ */
58
+ function percentText(ratio) {
59
+ if (ratio === null) return '';
60
+ const percent = ratio * 100;
61
+ return Math.abs(percent - Math.round(percent)) < 0.05 ? String(Math.round(percent)) : percent.toFixed(1);
62
+ }
63
+
64
+ /**
65
+ * 只算交接线上限:参考线 − marginPp,向下取整到整数百分位,再夹到 limit。
66
+ *
67
+ * 浮点安全:`0.62 − 0.05` 在二进制里可能是 0.5700000000000001 / 0.5699999999999999,
68
+ * 直接 floor 会把 57 变成 56,所以加 EPS 再 floor。
69
+ *
70
+ * @param {number|null} referenceRatio 参考线(实际触发线或配置阈值),非法时返回 null
71
+ * @param {{marginPp?:number, limit?:number}} [options]
72
+ * @returns {number|null} 例如 0.62;参考线太小(减完 ≤0)时返回 null
73
+ */
74
+ export function handoffCapOf(referenceRatio, { marginPp = HANDOFF_MARGIN_PP, limit = HANDOFF_LIMIT_DEFAULT } = {}) {
75
+ const reference = ratioOf(referenceRatio);
76
+ if (reference === null) return null;
77
+ const margin = (Number.isFinite(marginPp) ? marginPp : HANDOFF_MARGIN_PP) / 100;
78
+ const max = Number.isFinite(limit) && limit > 0 && limit < 1 ? limit : HANDOFF_LIMIT_DEFAULT;
79
+ // 向下取整到整数百分位(例如 62.8 → 0.62),不是四舍五入。
80
+ const cap = Math.floor((reference - margin) * 100 + EPS) / 100;
81
+ if (!(cap > 0)) return null;
82
+ return Math.min(cap, max);
83
+ }
84
+
85
+ /** 「重算目标 ≤ 压缩线」时的补充提示(保持「交接线 > 压缩线」现有约束)。 */
86
+ function compactConflictText(capPercent, compactRatio) {
87
+ return `按实际线重算目标 ${capPercent}% 不高于压缩线 ${percentText(compactRatio)}%,请先把压缩线降到 ${capPercent}% 以下。`;
88
+ }
89
+
90
+ /**
91
+ * 判定「当前交接线是否会在 DSH 原生压缩之后」。
92
+ *
93
+ * @param {object} [input]
94
+ * @param {number|null} [input.actualRatio] 当前模型实际触发线(state.native.ratio);算不出/关闭时 null
95
+ * @param {number|null} [input.configuredRatio] 配置阈值(native.thresholdRatio),兜底用
96
+ * @param {number|null} [input.handoffRatio] 当前交接提醒线(thresholds.handoffRatio)
97
+ * @param {number|null} [input.compactRatio] 压缩提醒线(thresholds.compactRatio),判重算冲突
98
+ * @param {boolean} [input.autoEnabled] DSH 原生自动压缩是否开启(false → 不判定,交接线无截止线)
99
+ * @param {number} [input.limit] cap 绝对上限,默认 0.95
100
+ * @param {'none'|'configured'} [input.fallback]
101
+ * 'none'(运行时默认):实际线算不出就不判定(不告警、不阻断);
102
+ * 'configured'(服务端保存默认):实际线算不出时按配置阈值 −1pp 防呆(0.4.1 行为)。
103
+ * @returns {{
104
+ * source:'actual'|'configured'|null,
105
+ * actualRatio:number|null, actualPercent:number|null,
106
+ * configuredRatio:number|null, configuredPercent:number|null,
107
+ * handoffRatio:number|null, handoffPercent:number|null,
108
+ * compactRatio:number|null, compactPercent:number|null,
109
+ * cap:number|null, capPercent:number|null,
110
+ * ok:boolean, severity:'ok'|'warn'|'late'|'unknown',
111
+ * reason:'ok'|'late'|'margin'|'fallback'|'no-reference'|'invalid-handoff'|'no-cap',
112
+ * message:string, target:number|null, compactConflict:boolean
113
+ * }}
114
+ */
115
+ export function handoffGuard({
116
+ actualRatio = null,
117
+ configuredRatio = null,
118
+ handoffRatio = null,
119
+ compactRatio = null,
120
+ autoEnabled = true,
121
+ limit = HANDOFF_LIMIT_DEFAULT,
122
+ fallback = 'none',
123
+ } = {}) {
124
+ const actual = ratioOf(actualRatio);
125
+ const configured = ratioOf(configuredRatio);
126
+ const handoff = ratioOf(handoffRatio);
127
+ const compact = ratioOf(compactRatio);
128
+ const neutral = {
129
+ source: null,
130
+ actualRatio: actual,
131
+ actualPercent: percentOf(actual),
132
+ configuredRatio: configured,
133
+ configuredPercent: percentOf(configured),
134
+ handoffRatio: handoff,
135
+ handoffPercent: percentOf(handoff),
136
+ compactRatio: compact,
137
+ compactPercent: percentOf(compact),
138
+ cap: null,
139
+ capPercent: null,
140
+ ok: true,
141
+ severity: 'unknown',
142
+ reason: 'no-reference',
143
+ message: '',
144
+ target: null,
145
+ compactConflict: false,
146
+ };
147
+ // 原生自动压缩关着:DSH 不会自己压,不存在「提醒来不及」的截止线。
148
+ if (autoEnabled === false) return neutral;
149
+
150
+ /** 带 cap 的结果骨架(cap 已是整数百分位,×100 即整数)。 */
151
+ const withCap = (source, cap) => {
152
+ const capPercent = cap === null ? null : Math.round(cap * 100);
153
+ return {
154
+ ...neutral,
155
+ source,
156
+ cap,
157
+ capPercent,
158
+ target: source === 'actual' ? cap : null,
159
+ compactConflict: cap !== null && compact !== null && cap <= compact + EPS,
160
+ };
161
+ };
162
+
163
+ // 1) 实际触发线可用:唯一口径(−5pp),不再叠加「配置阈值 −1pp」。
164
+ if (actual !== null) {
165
+ const cap = handoffCapOf(actual, { limit });
166
+ if (cap === null) return { ...withCap('actual', null), reason: 'no-cap' };
167
+ const base = withCap('actual', cap);
168
+ if (handoff === null) return { ...base, reason: 'invalid-handoff' };
169
+ const conflict = base.compactConflict ? compactConflictText(base.capPercent, compact) : '';
170
+ // 等于实际触发线也算「晚」:DSH 会在同一位置自己压历史,提醒来不及。
171
+ if (handoff > actual - EPS) {
172
+ return {
173
+ ...base,
174
+ ok: false,
175
+ severity: 'late',
176
+ reason: 'late',
177
+ message:
178
+ `交接线已晚:当前模型实际触发 ${percentText(actual)}%,交接线 ${percentText(handoff)}% 会在原生压缩之后` +
179
+ `(DSH 会先压历史);最高可设 ${base.capPercent}%。${conflict}`,
180
+ };
181
+ }
182
+ if (handoff > cap + EPS) {
183
+ return {
184
+ ...base,
185
+ ok: false,
186
+ severity: 'warn',
187
+ reason: 'margin',
188
+ message:
189
+ `交接线偏晚:当前模型实际触发 ${percentText(actual)}%,交接线 ${percentText(handoff)}% ` +
190
+ `距原生压缩不足 ${HANDOFF_MARGIN_PP} 个百分点;最高可设 ${base.capPercent}%。${conflict}`,
191
+ };
192
+ }
193
+ return { ...base, ok: true, severity: 'ok', reason: 'ok' };
194
+ }
195
+
196
+ // 2) 实际线算不出 + 服务端保存:退回配置阈值防呆(0.4.1 行为,兼容旧客户端)。
197
+ if (fallback === 'configured' && configured !== null) {
198
+ const cap = handoffCapOf(configured, { marginPp: FALLBACK_MARGIN_PP, limit });
199
+ if (cap === null) return { ...withCap('configured', null), reason: 'no-cap' };
200
+ const base = withCap('configured', cap);
201
+ if (handoff === null) return { ...base, reason: 'invalid-handoff' };
202
+ if (handoff > cap + EPS) {
203
+ return {
204
+ ...base,
205
+ ok: false,
206
+ severity: 'warn',
207
+ reason: 'fallback',
208
+ message:
209
+ `「交接提醒线」必须低于 DSH 原生自动压缩线(配置阈值 ${percentText(configured)}%),` +
210
+ `否则原生压缩会先动手;最高只能设到 ${base.capPercent}%。` +
211
+ '想设更高请先在设置里关闭「DSH 原生自动压缩」。',
212
+ };
213
+ }
214
+ return { ...base, ok: true, severity: 'ok', reason: 'ok' };
215
+ }
216
+
217
+ // 3) 运行时且算不出:不告警、不阻断。
218
+ return neutral;
219
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-compact-relay",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "DeepSeek Harness 上下文指示器插件:提供容量监控、压缩预览与确认、九段式交接单生成及消息锁定功能。 · Context indicator for DSH: preview-then-confirm compaction, 9-section session handoff, and pinned messages.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -53,9 +53,10 @@
53
53
  "node": ">=22"
54
54
  },
55
55
  "scripts": {
56
- "test": "node scripts/build-client.mjs --check && node --check lib/client.js && node test/smoke.mjs && node test/handoff.mjs && node test/plugin-load.mjs && node test/contract.mjs && node test/compact-log.mjs && node test/compress-cue.mjs && node test/state-passive.mjs && node test/native-compaction.mjs && node test/markdown-lite.mjs && node test/diagnostics.mjs && node test/pin-contract.mjs && node test/ledger.mjs && node test/handoff-p2.mjs && node test/native-line.mjs",
56
+ "test": "node scripts/build-client.mjs --check && node --check lib/client.js && node test/smoke.mjs && node test/handoff.mjs && node test/plugin-load.mjs && node test/contract.mjs && node test/compact-log.mjs && node test/compress-cue.mjs && node test/state-passive.mjs && node test/native-compaction.mjs && node test/markdown-lite.mjs && node test/diagnostics.mjs && node test/pin-contract.mjs && node test/ledger.mjs && node test/handoff-p2.mjs && node test/native-line.mjs && node test/handoff-guard.mjs && node test/settings-ux.mjs",
57
57
  "test:e2e": "node test/e2e/smoke.mjs",
58
58
  "test:e2e:settings": "node test/e2e/settings-ui.mjs",
59
+ "test:e2e:settings-ux": "node test/e2e/settings-ux.mjs",
59
60
  "test:e2e:compact": "node test/e2e/real-compact.mjs"
60
61
  },
61
62
  "author": "cfanzx <cfanzx@users.noreply.github.com>",