dsh-compact-relay 0.3.6 → 0.4.0
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/CHANGELOG.md +76 -3
- package/README.en.md +18 -1
- package/README.md +20 -3
- package/ROADMAP.md +9 -2
- package/lib/actions/better-compact.js +38 -1
- package/lib/actions/compact-run.js +29 -3
- package/lib/actions/compress-hint.js +21 -1
- package/lib/actions/diagnostics.js +130 -0
- package/lib/actions/handoff.js +90 -11
- package/lib/actions/pin.js +6 -23
- package/lib/actions/state.js +47 -5
- package/lib/adapters/http.js +70 -7
- package/lib/adapters/prompt.js +71 -10
- package/lib/adapters/tool.js +9 -4
- package/lib/client.js +485 -13
- package/lib/core/compress-cue.js +48 -16
- package/lib/core/handoff-doc.js +4 -0
- package/lib/core/handoff-edit.js +86 -0
- package/lib/core/handoff-facts.js +17 -2
- package/lib/core/handoff-spec.js +18 -0
- package/lib/core/ledger.js +176 -0
- package/lib/core/native-line.js +65 -0
- package/lib/core/pin-anchors.js +93 -0
- package/lib/core/pin-suggest.js +129 -0
- package/lib/core/policy.js +16 -0
- package/lib/dsh/diag-counters.js +204 -0
- package/lib/dsh/diagnostics.js +527 -0
- package/lib/dsh/handoff-writer.js +7 -5
- package/lib/dsh/ledger.js +108 -0
- package/lib/dsh/native-compaction.js +128 -0
- package/lib/dsh/pressure.js +62 -10
- package/lib/dsh/relay.js +23 -6
- package/lib/dsh/session-view.js +32 -4
- package/lib/dsh/sessions.js +1 -1
- package/lib/index.js +6 -0
- package/package.json +2 -2
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
* thresholdRatio: 0.8(80% 触发)
|
|
7
7
|
* 用户可以在 profile 的 `cordis.patch.yml` 里按 `- id: compaction-basic` 覆盖。
|
|
8
8
|
*
|
|
9
|
+
* 0.4 P2-3:解析 `headroomTokens` 与 `modelPolicies`,供「真实触发线」计算
|
|
10
|
+
* (thresholdTokens = floor(min(W × ratio, W − 输出预留 − headroom)),见 core/native-line.js)。
|
|
11
|
+
*
|
|
9
12
|
* 本模块只做两件事:
|
|
10
13
|
* 1. 读当前��效值(用户覆盖 + 默认值),供指示灯/设置页显示;
|
|
11
14
|
* 2. 写开关(关闭/恢复自动压缩)——追加或修改那一条,**写前备份**、原子替换。
|
|
@@ -17,6 +20,8 @@ import fs from 'node:fs';
|
|
|
17
20
|
import os from 'node:os';
|
|
18
21
|
import path from 'node:path';
|
|
19
22
|
|
|
23
|
+
import { NATIVE_HEADROOM_DEFAULT } from '../core/native-line.js';
|
|
24
|
+
|
|
20
25
|
export const NATIVE_PLUGIN_ID = 'compaction-basic';
|
|
21
26
|
export const NATIVE_PLUGIN_NAME = '@deepseek-ai/dsh-compaction-basic';
|
|
22
27
|
/** DSH 默认值(compaction-basic 源码:auto ?? true、thresholdRatio ?? 0.8)。 */
|
|
@@ -65,6 +70,11 @@ const ENTRY_ID_RE = /^(\s*)-\s+id:\s*['"]?compaction-basic['"]?\s*(?:#.*)?$/;
|
|
|
65
70
|
const CONFIG_RE = /^(\s*)config\s*:\s*(?:#.*)?$/;
|
|
66
71
|
const AUTO_RE = /^auto\s*:\s*(true|false)\s*(?:#.*)?$/;
|
|
67
72
|
const RATIO_RE = /^thresholdRatio\s*:\s*['"]?([0-9.]+)['"]?\s*(?:#.*)?$/;
|
|
73
|
+
const HEADROOM_RE = /^headroomTokens\s*:\s*['"]?(\d+)['"]?\s*(?:#.*)?$/;
|
|
74
|
+
/** modelPolicies 列表项里的键(- provider: x / model: y / thresholdRatio: 0.5 / headroomTokens: 4096)。 */
|
|
75
|
+
const POLICY_PROVIDER_RE = /^provider\s*:\s*['"]?([^'"#\s]+)['"]?\s*(?:#.*)?$/;
|
|
76
|
+
const POLICY_MODEL_RE = /^model\s*:\s*['"]?([^'"#\s]+)['"]?\s*(?:#.*)?$/;
|
|
77
|
+
const POLICY_ITEM_RE = /^(\s*)-\s+(.*)$/;
|
|
68
78
|
|
|
69
79
|
/** 按行拆文本,保留原换行风格(CRLF 文件 apply 后仍是 CRLF)。 */
|
|
70
80
|
function splitText(text) {
|
|
@@ -177,9 +187,124 @@ export function parseCompactionOverride(text) {
|
|
|
177
187
|
if (Number.isFinite(value) && value > 0 && value <= 1) override.thresholdRatio = value;
|
|
178
188
|
}
|
|
179
189
|
}
|
|
190
|
+
// 0.4 P2-3:headroomTokens 也是 config 直属子键(全局余量,默认 65536)。
|
|
191
|
+
const headroomIndex = findDirectChild(lines, entry, config, HEADROOM_RE);
|
|
192
|
+
if (headroomIndex >= 0) {
|
|
193
|
+
const match = HEADROOM_RE.exec(lines[headroomIndex].trim());
|
|
194
|
+
if (match !== null) {
|
|
195
|
+
const value = Number(match[1]);
|
|
196
|
+
if (Number.isInteger(value) && value >= 0) override.headroomTokens = value;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
180
199
|
return override;
|
|
181
200
|
}
|
|
182
201
|
|
|
202
|
+
/**
|
|
203
|
+
* 0.4 P2-3:解析 `config.modelPolicies`(嵌套层,按 provider+model 精确匹配的路由级覆盖)。
|
|
204
|
+
*
|
|
205
|
+
* 支持 DSH Loader 的两种 YAML 写法(Win 真实配置用的是列表写法):
|
|
206
|
+
* modelPolicies:
|
|
207
|
+
* - provider: mac-unc
|
|
208
|
+
* model: qwen35-unc-64k
|
|
209
|
+
* headroomTokens: 4096
|
|
210
|
+
* 或映射写法(key 当 model):
|
|
211
|
+
* modelPolicies:
|
|
212
|
+
* mac-unc:
|
|
213
|
+
* thresholdRatio: 0.5
|
|
214
|
+
*
|
|
215
|
+
* 只收 `thresholdRatio`(0–1)与 `headroomTokens`(≥0);其它键忽略。
|
|
216
|
+
* @param {string} text
|
|
217
|
+
* @returns {Array<{provider:string|null, model:string|null, thresholdRatio?:number, headroomTokens?:number}>}
|
|
218
|
+
*/
|
|
219
|
+
export function parseModelPolicies(text) {
|
|
220
|
+
if (typeof text !== 'string' || text.length === 0) return [];
|
|
221
|
+
const { lines } = splitText(text);
|
|
222
|
+
const entry = locateEntry(lines);
|
|
223
|
+
if (entry === null) return [];
|
|
224
|
+
const config = locateConfig(lines, entry);
|
|
225
|
+
if (config === null || config.childIndent === null) return [];
|
|
226
|
+
const start = findDirectChild(lines, entry, config, /^modelPolicies\s*:\s*(?:#.*)?$/);
|
|
227
|
+
if (start < 0) return [];
|
|
228
|
+
const blockIndent = indentOf(lines[start]);
|
|
229
|
+
const policies = [];
|
|
230
|
+
let current = null;
|
|
231
|
+
const flush = () => {
|
|
232
|
+
if (current === null) return;
|
|
233
|
+
if (current.thresholdRatio !== undefined || current.headroomTokens !== undefined) policies.push(current);
|
|
234
|
+
current = null;
|
|
235
|
+
};
|
|
236
|
+
for (let index = start + 1; index < entry.end; index += 1) {
|
|
237
|
+
const line = lines[index];
|
|
238
|
+
if (line.trim().length === 0 || /^\s*#/.test(line)) continue;
|
|
239
|
+
const indent = indentOf(line);
|
|
240
|
+
if (indent <= blockIndent) break;
|
|
241
|
+
const item = POLICY_ITEM_RE.exec(line);
|
|
242
|
+
if (item !== null && indent > blockIndent) {
|
|
243
|
+
// 列表项:- provider: x 或 - model: y(同缩进的新项)
|
|
244
|
+
if (item[1].length <= blockIndent + 2) { flush(); current = { provider: null, model: null }; }
|
|
245
|
+
const rest = item[2].trim();
|
|
246
|
+
const pm = POLICY_PROVIDER_RE.exec(rest);
|
|
247
|
+
const mm = POLICY_MODEL_RE.exec(rest);
|
|
248
|
+
if (current === null) current = { provider: null, model: null };
|
|
249
|
+
if (pm !== null) current.provider = pm[1];
|
|
250
|
+
else if (mm !== null) current.model = mm[1];
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const trimmed = line.trim();
|
|
254
|
+
const ratioMatch = RATIO_RE.exec(trimmed);
|
|
255
|
+
const headMatch = HEADROOM_RE.exec(trimmed);
|
|
256
|
+
const providerMatch = POLICY_PROVIDER_RE.exec(trimmed);
|
|
257
|
+
const modelMatch = POLICY_MODEL_RE.exec(trimmed);
|
|
258
|
+
if (ratioMatch !== null || headMatch !== null || providerMatch !== null || modelMatch !== null) {
|
|
259
|
+
if (current === null) current = { provider: null, model: null };
|
|
260
|
+
if (ratioMatch !== null) {
|
|
261
|
+
const value = Number(ratioMatch[1]);
|
|
262
|
+
if (Number.isFinite(value) && value > 0 && value <= 1) current.thresholdRatio = value;
|
|
263
|
+
} else if (headMatch !== null) {
|
|
264
|
+
const value = Number(headMatch[1]);
|
|
265
|
+
if (Number.isInteger(value) && value >= 0) current.headroomTokens = value;
|
|
266
|
+
} else if (providerMatch !== null) current.provider = providerMatch[1];
|
|
267
|
+
else current.model = modelMatch[1];
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
// 映射写法:更深一层的 key 行当 model,其下缩进再深的键值由后续循环计入(current 不变)。
|
|
271
|
+
const keyMatch = /^([A-Za-z0-9._@/-]+)\s*:\s*(?:#.*)?$/.exec(trimmed);
|
|
272
|
+
if (keyMatch !== null && indent === blockIndent + 2 && current === null) {
|
|
273
|
+
current = { provider: null, model: keyMatch[1] };
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
flush();
|
|
277
|
+
return policies;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* 0.4 P2-3:按路由取生效的 compaction-basic 策略(全局值 + 精确 provider/model 覆盖)。
|
|
282
|
+
* @param {{thresholdRatio:number, headroomTokens:number, modelPolicies?:Array}} nativeCfg readNativeCompaction() 结果
|
|
283
|
+
* @param {{provider:string, model:string}|null} route
|
|
284
|
+
* @returns {{thresholdRatio:number, headroomTokens:number, overridden:boolean}}
|
|
285
|
+
*/
|
|
286
|
+
export function resolveNativePolicy(nativeCfg, route) {
|
|
287
|
+
const globalRatio = Number.isFinite(nativeCfg?.thresholdRatio) ? nativeCfg.thresholdRatio : NATIVE_DEFAULTS.thresholdRatio;
|
|
288
|
+
const globalHeadroom = Number.isFinite(nativeCfg?.headroomTokens) ? nativeCfg.headroomTokens : NATIVE_HEADROOM_DEFAULT;
|
|
289
|
+
if (route === null || route === undefined || typeof route.provider !== 'string' || typeof route.model !== 'string') {
|
|
290
|
+
return { thresholdRatio: globalRatio, headroomTokens: globalHeadroom, overridden: false };
|
|
291
|
+
}
|
|
292
|
+
const list = Array.isArray(nativeCfg?.modelPolicies) ? nativeCfg.modelPolicies : [];
|
|
293
|
+
const hit = list.find(
|
|
294
|
+
(policy) =>
|
|
295
|
+
policy !== null &&
|
|
296
|
+
typeof policy === 'object' &&
|
|
297
|
+
(policy.model === route.model || policy.model === `${route.provider}/${route.model}` || policy.model === null || policy.model === undefined) &&
|
|
298
|
+
(policy.provider === undefined || policy.provider === null || policy.provider === route.provider),
|
|
299
|
+
);
|
|
300
|
+
if (hit === undefined) return { thresholdRatio: globalRatio, headroomTokens: globalHeadroom, overridden: false };
|
|
301
|
+
return {
|
|
302
|
+
thresholdRatio: Number.isFinite(hit.thresholdRatio) ? hit.thresholdRatio : globalRatio,
|
|
303
|
+
headroomTokens: Number.isFinite(hit.headroomTokens) ? hit.headroomTokens : globalHeadroom,
|
|
304
|
+
overridden: true,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
183
308
|
/**
|
|
184
309
|
* 读当前生效的原生压缩配置(用户覆盖 + 默认值)。带 mtime 缓存,避免每帧 stat 读文件。
|
|
185
310
|
*
|
|
@@ -204,6 +329,9 @@ export function readNativeCompaction(profileDir = resolveProfileDir()) {
|
|
|
204
329
|
const value = {
|
|
205
330
|
autoEnabled: override.autoEnabled ?? NATIVE_DEFAULTS.autoEnabled,
|
|
206
331
|
thresholdRatio: override.thresholdRatio ?? NATIVE_DEFAULTS.thresholdRatio,
|
|
332
|
+
// 0.4 P2-3:真实触发线要用的余量与路由级覆盖(additive 字段,老调用方不受影响)。
|
|
333
|
+
headroomTokens: override.headroomTokens ?? NATIVE_HEADROOM_DEFAULT,
|
|
334
|
+
modelPolicies: parseModelPolicies(text),
|
|
207
335
|
override,
|
|
208
336
|
file,
|
|
209
337
|
};
|
package/lib/dsh/pressure.js
CHANGED
|
@@ -7,12 +7,14 @@
|
|
|
7
7
|
import { ratioOf } from '../core/policy.js';
|
|
8
8
|
import { routeOf } from './route.js';
|
|
9
9
|
|
|
10
|
-
/** provider/model →
|
|
10
|
+
/** provider/model → {window, defaultMaxTokens} 的进程内缓存(Promise,避免并发重复解析)。 */
|
|
11
11
|
const windowCache = new Map();
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
/**
|
|
14
|
+
* 解析路由模型的容量信息(contextWindow + 适配器默认输出上限)。
|
|
15
|
+
* 0.4 P2-3:输出预留要先看 requestHeader 的 maxTokens,其次才是这里的 defaultMaxTokens。
|
|
16
|
+
*/
|
|
17
|
+
async function resolveModelMeta(ctx, route) {
|
|
16
18
|
const key = `${route.provider}/${route.model}`;
|
|
17
19
|
let pending = windowCache.get(key);
|
|
18
20
|
if (pending === undefined) {
|
|
@@ -20,18 +22,68 @@ async function resolveWindow(ctx, session, agent, fallback) {
|
|
|
20
22
|
try {
|
|
21
23
|
const info = await ctx.llm.resolveModelInfo(route.provider, route.model);
|
|
22
24
|
const window = info?.context?.contextWindow;
|
|
23
|
-
|
|
25
|
+
const def = info?.defaultMaxTokens;
|
|
26
|
+
return {
|
|
27
|
+
window: Number.isInteger(window) && window > 0 ? window : null,
|
|
28
|
+
defaultMaxTokens: Number.isInteger(def) && def >= 0 ? def : null,
|
|
29
|
+
};
|
|
24
30
|
} catch {
|
|
25
|
-
return null;
|
|
31
|
+
return { window: null, defaultMaxTokens: null };
|
|
26
32
|
}
|
|
27
33
|
})();
|
|
28
34
|
windowCache.set(key, pending);
|
|
29
35
|
}
|
|
30
|
-
const
|
|
31
|
-
//
|
|
36
|
+
const meta = await pending;
|
|
37
|
+
// 失败不留在缓存里:适配器还没就绪时第一次解析失败,不能永久用 fallback
|
|
32
38
|
// 窗口算比例(2026-10-19 debug:指示灯百分比长期低估)。
|
|
33
|
-
if (
|
|
34
|
-
return
|
|
39
|
+
if (meta.window === null) windowCache.delete(key);
|
|
40
|
+
return meta;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function resolveWindow(ctx, session, agent, fallback) {
|
|
44
|
+
const route = routeOf(ctx, session, agent);
|
|
45
|
+
if (route === null) return fallback;
|
|
46
|
+
const meta = await resolveModelMeta(ctx, route);
|
|
47
|
+
return meta.window ?? fallback;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 0.4 P2-3:解析「真实触发线」需要的路由预算。
|
|
52
|
+
* - contextWindow:路由模型窗口(失败回落测量窗口);
|
|
53
|
+
* - reservedCompletionTokens = session.requestHeader().config.maxTokens ?? 适配器 defaultMaxTokens ?? 0
|
|
54
|
+
* (与 dsh-compaction-basic 的 reservedCompletionTokens 同一口径)。
|
|
55
|
+
* @param {object} ctx
|
|
56
|
+
* @param {object} session
|
|
57
|
+
* @param {object} [agent]
|
|
58
|
+
* @param {number} [fallbackWindow]
|
|
59
|
+
* @returns {Promise<{route:object|null, contextWindow:number|null, reservedCompletionTokens:number, defaultMaxTokens:number|null, requestMaxTokens:number|null}>}
|
|
60
|
+
*/
|
|
61
|
+
export async function routeBudgetOf(ctx, session, agent, fallbackWindow) {
|
|
62
|
+
const route = routeOf(ctx, session, agent);
|
|
63
|
+
let meta = { window: null, defaultMaxTokens: null };
|
|
64
|
+
if (route !== null) {
|
|
65
|
+
try {
|
|
66
|
+
meta = await resolveModelMeta(ctx, route);
|
|
67
|
+
} catch {
|
|
68
|
+
/* ignore */
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
let headerMax = null;
|
|
72
|
+
try {
|
|
73
|
+
const value = session?.requestHeader?.()?.config?.maxTokens;
|
|
74
|
+
if (Number.isInteger(value) && value >= 0) headerMax = value;
|
|
75
|
+
} catch {
|
|
76
|
+
/* 老宿主没有 requestHeader 就按未知处理 */
|
|
77
|
+
}
|
|
78
|
+
const reserved = headerMax ?? meta.defaultMaxTokens ?? 0;
|
|
79
|
+
const window = meta.window ?? (Number.isFinite(fallbackWindow) && fallbackWindow > 0 ? fallbackWindow : null);
|
|
80
|
+
return {
|
|
81
|
+
route,
|
|
82
|
+
contextWindow: window,
|
|
83
|
+
reservedCompletionTokens: reserved,
|
|
84
|
+
defaultMaxTokens: meta.defaultMaxTokens,
|
|
85
|
+
requestMaxTokens: headerMax,
|
|
86
|
+
};
|
|
35
87
|
}
|
|
36
88
|
|
|
37
89
|
/**
|
package/lib/dsh/relay.js
CHANGED
|
@@ -9,21 +9,38 @@ import { RELAY_RECORD_TYPE } from '../core/relay.js';
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* 合法化一条 relay 记录;不合法返回 null。
|
|
12
|
+
*
|
|
13
|
+
* 0.4 P2-2(接力链元数据补全):canonical 字段为 `parentId` / `childId` / `ordinal` / `at` / `atIso`;
|
|
14
|
+
* 同时保留 0.3 的旧字段 `parentSessionId` / `index`(旧记录只读兼容,新写入两者都带)。
|
|
15
|
+
*
|
|
12
16
|
* @param {unknown} data
|
|
13
|
-
* @returns {{
|
|
17
|
+
* @returns {{parentId:string, childId:string, ordinal:number, at:number, atIso:string|null, parentSessionId:string, index:number, path?:string, planId?:string|null, title?:string, baseTitle?:string}|null}
|
|
14
18
|
*/
|
|
15
19
|
export function normalizeRelayRecord(data) {
|
|
16
20
|
if (data === null || typeof data !== 'object') return null;
|
|
17
21
|
if (typeof data.childId !== 'string' || data.childId.length === 0) return null;
|
|
18
|
-
const
|
|
19
|
-
|
|
22
|
+
const ordinal = Number.isInteger(data.ordinal) && data.ordinal > 0
|
|
23
|
+
? data.ordinal
|
|
24
|
+
: Number.isInteger(data.index) && data.index > 0
|
|
25
|
+
? data.index
|
|
26
|
+
: null;
|
|
27
|
+
if (ordinal === null) return null;
|
|
28
|
+
const parentId = typeof data.parentId === 'string'
|
|
29
|
+
? data.parentId
|
|
30
|
+
: typeof data.parentSessionId === 'string'
|
|
31
|
+
? data.parentSessionId
|
|
32
|
+
: '';
|
|
20
33
|
return {
|
|
21
|
-
|
|
34
|
+
parentId,
|
|
22
35
|
childId: data.childId,
|
|
23
|
-
|
|
36
|
+
ordinal,
|
|
37
|
+
at: Number.isFinite(data.at) ? data.at : 0,
|
|
38
|
+
atIso: typeof data.atIso === 'string' ? data.atIso : null,
|
|
39
|
+
// 0.3 旧字段兼容(测试/旧读法仍按这些名字取)。
|
|
40
|
+
parentSessionId: parentId,
|
|
41
|
+
index: ordinal,
|
|
24
42
|
...(typeof data.path === 'string' ? { path: data.path } : {}),
|
|
25
43
|
planId: typeof data.planId === 'string' ? data.planId : null,
|
|
26
|
-
at: Number.isFinite(data.at) ? data.at : 0,
|
|
27
44
|
...(typeof data.title === 'string' ? { title: data.title } : {}),
|
|
28
45
|
...(typeof data.baseTitle === 'string' ? { baseTitle: data.baseTitle } : {}),
|
|
29
46
|
};
|
package/lib/dsh/session-view.js
CHANGED
|
@@ -8,6 +8,30 @@ import { latestCompressHint } from '../core/compress-hint.js';
|
|
|
8
8
|
import { latestPinnedSeqs } from '../core/pins.js';
|
|
9
9
|
import { scanCompactionLog } from './compaction-pair.js';
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* 0.4 BUG-013:checkpoint 判定的默认实现(漏传第二个参数时也不再静默失效)。
|
|
13
|
+
*
|
|
14
|
+
* 背景:0.3.1 起 `lastCheckpointSurfaceIdx/Seq` 的第二个参数 `isCheckpointSource`
|
|
15
|
+
* 在部分调用点(如 actions/state.js)被漏传。`isCheckpointSource(source)` 对
|
|
16
|
+
* undefined 会抛 TypeError,而函数内 try/catch 把它吞成「不是 checkpoint」→ 永远
|
|
17
|
+
* 返回 -1 → `compressHint` 永远算「有效」(压缩/交接后旧断点不消失、还会抑制 cue)。
|
|
18
|
+
*
|
|
19
|
+
* 这里给默认值补上 engine 的实现(dsh→dsh 同层,不产生环;engine 不 import 本文件)。
|
|
20
|
+
* 但 engine 是全插件唯一 import `@deepseek-ai/*` 的模块:纯 node 单测
|
|
21
|
+
* (test/smoke.mjs / state-passive.mjs / handoff.mjs)不注册包解析钩子、加载不了
|
|
22
|
+
* 这些包。所以用受保护的动态 import 取默认实现:
|
|
23
|
+
* - 宿主环境:正常拿到 engine.isCompactCheckpointSource;
|
|
24
|
+
* - 纯 node 单测:加载失败则保持 null(函数里显式传入的 fake engine 照常优先)。
|
|
25
|
+
*/
|
|
26
|
+
let defaultIsCheckpointSource = null;
|
|
27
|
+
try {
|
|
28
|
+
const loaded = await import('./engine.js');
|
|
29
|
+
const candidate = loaded?.default?.isCompactCheckpointSource;
|
|
30
|
+
if (typeof candidate === 'function') defaultIsCheckpointSource = candidate;
|
|
31
|
+
} catch {
|
|
32
|
+
defaultIsCheckpointSource = null;
|
|
33
|
+
}
|
|
34
|
+
|
|
11
35
|
/** @returns {number[]} 当前 surface 的 event seq(升序) */
|
|
12
36
|
export function surfaceSeqs(session) {
|
|
13
37
|
const nodes = session?.surface?.nodes;
|
|
@@ -68,9 +92,13 @@ export function countSummaries(session) {
|
|
|
68
92
|
/**
|
|
69
93
|
* 最后一次 checkpoint 消息在 surface 中的下标;没有返回 -1。
|
|
70
94
|
* @param {object} session
|
|
71
|
-
* @param {(source: unknown) => boolean} isCheckpointSource
|
|
95
|
+
* @param {(source: unknown) => boolean} [isCheckpointSource] 省略时用 engine.isCompactCheckpointSource
|
|
96
|
+
* @returns {number}
|
|
72
97
|
*/
|
|
73
|
-
export function lastCheckpointSurfaceIdx(session, isCheckpointSource) {
|
|
98
|
+
export function lastCheckpointSurfaceIdx(session, isCheckpointSource = defaultIsCheckpointSource) {
|
|
99
|
+
// 既没显式传、默认实现也没加载到(纯 node 单测):按「没有 checkpoint」处理,
|
|
100
|
+
// 不再让 TypeError 在循环里被静默吞掉。
|
|
101
|
+
if (typeof isCheckpointSource !== 'function') return -1;
|
|
74
102
|
const seqs = surfaceSeqs(session);
|
|
75
103
|
for (let index = seqs.length - 1; index >= 0; index -= 1) {
|
|
76
104
|
const event = typeof session.eventAt === 'function' ? session.eventAt(seqs[index]) : undefined;
|
|
@@ -119,10 +147,10 @@ export function livePinnedSeqs(session) {
|
|
|
119
147
|
* 按下标切会把这段旧尾部又压一遍(2026-10-19 真机踩过:33 条旧消息被误压)。
|
|
120
148
|
*
|
|
121
149
|
* @param {object} session
|
|
122
|
-
* @param {(source: unknown) => boolean} isCheckpointSource
|
|
150
|
+
* @param {(source: unknown) => boolean} [isCheckpointSource] 省略时用 engine.isCompactCheckpointSource
|
|
123
151
|
* @returns {number|undefined}
|
|
124
152
|
*/
|
|
125
|
-
export function lastCheckpointSurfaceSeq(session, isCheckpointSource) {
|
|
153
|
+
export function lastCheckpointSurfaceSeq(session, isCheckpointSource = defaultIsCheckpointSource) {
|
|
126
154
|
const index = lastCheckpointSurfaceIdx(session, isCheckpointSource);
|
|
127
155
|
if (index < 0) return undefined;
|
|
128
156
|
return surfaceSeqs(session)[index];
|
package/lib/dsh/sessions.js
CHANGED
|
@@ -81,7 +81,7 @@ export function handoffTitlePlanOf(ctx, session) {
|
|
|
81
81
|
if (record !== null) {
|
|
82
82
|
const fallback = splitHandoffTitle(sourceTitleOf(ctx, session));
|
|
83
83
|
const baseTitle = typeof record.baseTitle === 'string' && record.baseTitle.length > 0 ? record.baseTitle : fallback.baseTitle;
|
|
84
|
-
const index = record.index + 1;
|
|
84
|
+
const index = (Number.isInteger(record.ordinal) ? record.ordinal : record.index) + 1;
|
|
85
85
|
return { index, baseTitle, title: relayTitleOf(index, baseTitle), viaRecord: true };
|
|
86
86
|
}
|
|
87
87
|
const split = splitHandoffTitle(sourceTitleOf(ctx, session));
|
package/lib/index.js
CHANGED
|
@@ -12,7 +12,9 @@ import { registerHttp } from './adapters/http.js';
|
|
|
12
12
|
import { registerCompressCue } from './adapters/prompt.js';
|
|
13
13
|
import { registerTool } from './adapters/tool.js';
|
|
14
14
|
import { createSettingsStore } from './actions/settings.js';
|
|
15
|
+
import { createDiagCounters } from './dsh/diag-counters.js';
|
|
15
16
|
import engine from './dsh/engine.js';
|
|
17
|
+
import { createLedgerStore } from './dsh/ledger.js';
|
|
16
18
|
import { createStateHub } from './dsh/state-hub.js';
|
|
17
19
|
|
|
18
20
|
export const name = 'compact-relay';
|
|
@@ -53,6 +55,10 @@ export function apply(ctx, config) {
|
|
|
53
55
|
lock: createLock(),
|
|
54
56
|
// M6:进行中动作登记表(HTTP cancel 用),与 lock 的分工见 actions/inflight.js。
|
|
55
57
|
inflight: createInflight(),
|
|
58
|
+
// 0.4 P0-2:诊断计数/耗时/错误码(纯内存,重启清零;见 dsh/diag-counters.js)。
|
|
59
|
+
diag: createDiagCounters(),
|
|
60
|
+
// 0.4 P1-3:轻账本(~/.dsh/compact-relay-ledger.jsonl;写失败静默降级,不影响动作)。
|
|
61
|
+
ledger: createLedgerStore(),
|
|
56
62
|
engine,
|
|
57
63
|
...(log === undefined ? {} : { log }),
|
|
58
64
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-compact-relay",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
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,7 +53,7 @@
|
|
|
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",
|
|
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",
|
|
57
57
|
"test:e2e": "node test/e2e/smoke.mjs",
|
|
58
58
|
"test:e2e:settings": "node test/e2e/settings-ui.mjs",
|
|
59
59
|
"test:e2e:compact": "node test/e2e/real-compact.mjs"
|