@nightpainters/dsh-ling 1.5.3 → 1.6.1
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 +121 -6
- package/DESIGN.md +8 -4
- package/README.md +181 -7
- package/lib/client.js +35 -35
- package/lib/host/api.js +29 -22
- package/lib/host/archive.js +15 -9
- package/lib/host/autotree.js +2 -2
- package/lib/host/clock-skill.js +1 -1
- package/lib/host/deep-index-skill.js +111 -0
- package/lib/host/inject.js +1 -1
- package/lib/host/l1.js +350 -23
- package/lib/host/lifecycle.js +76 -10
- package/lib/host/memory.js +323 -37
- package/lib/host/persona.js +2 -2
- package/lib/host/rawlog.js +323 -0
- package/lib/host/recall-search.js +210 -0
- package/lib/host/retitle.js +5 -5
- package/lib/host/snapshot.js +65 -13
- package/lib/host/summarizer.js +10 -4
- package/lib/host/surgery.js +2 -2
- package/lib/host/tools.js +327 -14
- package/lib/host/tree-ops.js +12 -6
- package/lib/host/util.js +1 -1
- package/lib/index.js +41 -2
- package/package.json +6 -2
package/lib/host/lifecycle.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// dsh-ling host — lifecycle wiring
|
|
1
|
+
// dsh-ling host — lifecycle wiring(事件的接线层;DESIGN.md §4.4 有本节的结构说明):
|
|
2
2
|
// session events → freeze gate state, snapshot freeze, incremental capture.
|
|
3
3
|
// Every handler is defensive (payload shapes verified at M1b live probe).
|
|
4
4
|
import { svc, pick, isMemoryEligibleHeader, sessionKind, utcIso } from './util.js';
|
|
@@ -8,6 +8,38 @@ import { summarizeDsh } from './summarizer.js';
|
|
|
8
8
|
import { extractMessageText, messageSourceKind } from './snapshot.js';
|
|
9
9
|
import { currentMode } from './mode.js';
|
|
10
10
|
|
|
11
|
+
// ── 丢弃计数(2026-10-04,P1 第三件)──────────────────────────────────────────────
|
|
12
|
+
// 病灶:两条写入路径的**过滤全是静默的** —— 只记"写进去了多少",不记"丢掉了多少、为什么丢"。
|
|
13
|
+
// 于是"库里为什么只有 1.8% 的现场"这个问题,只能靠事后拿磁盘日志逐事件重放才答得出来
|
|
14
|
+
// (2026-10-04 的 P1 侦察就是这么干的,花了一整路子代理)。主人拍板"P1 三件做",这是其中的可观测项。
|
|
15
|
+
//
|
|
16
|
+
// 落点 `dbg.event_stats` = 本轮(重启以来)扫过多少、按类型/来源/空正文/无会话/子代理各丢多少。
|
|
17
|
+
// ⚠️ Path A 是**逐事件**回调 ⇒ 计数先在内存累加,每 `EV_STATS_FLUSH_EVERY` 次或 `EV_STATS_FLUSH_MS`
|
|
18
|
+
// 才落一次 kv —— 否则会给每个事件加两次 sqlite 往返,而这条路径在每个会话里都极高频。
|
|
19
|
+
const EV_STATS_FLUSH_EVERY = 20;
|
|
20
|
+
const EV_STATS_FLUSH_MS = 5000;
|
|
21
|
+
const evStats = { seen: 0, kept: 0, dropType: 0, dropSource: 0, dropEmpty: 0, dropNoSession: 0, dropSubagent: 0, types: {}, lastSid: '' };
|
|
22
|
+
let evStatsDirty = 0;
|
|
23
|
+
let evStatsAt = 0;
|
|
24
|
+
/** 落盘(节流)。`force` 用于收尾/关键点。写不进去不影响主流程 —— 它只是诊断。 */
|
|
25
|
+
function flushEventStats(memory, force = false) {
|
|
26
|
+
evStatsDirty += 1;
|
|
27
|
+
const nowMs = Date.now();
|
|
28
|
+
if (!force && evStatsDirty < EV_STATS_FLUSH_EVERY && nowMs - evStatsAt < EV_STATS_FLUSH_MS) return;
|
|
29
|
+
evStatsDirty = 0;
|
|
30
|
+
evStatsAt = nowMs;
|
|
31
|
+
try {
|
|
32
|
+
const types = Object.entries(evStats.types).sort((a, b) => b[1] - a[1]).slice(0, 12);
|
|
33
|
+
memory.kvSet('dbg.event_stats', JSON.stringify({
|
|
34
|
+
at: new Date(nowMs).toISOString(),
|
|
35
|
+
seen: evStats.seen, kept: evStats.kept,
|
|
36
|
+
dropType: evStats.dropType, dropSource: evStats.dropSource, dropEmpty: evStats.dropEmpty,
|
|
37
|
+
dropNoSession: evStats.dropNoSession, dropSubagent: evStats.dropSubagent,
|
|
38
|
+
lastSid: evStats.lastSid, types: Object.fromEntries(types),
|
|
39
|
+
}).slice(0, 1500));
|
|
40
|
+
} catch { /* 诊断写不进去不影响主流程 */ }
|
|
41
|
+
}
|
|
42
|
+
|
|
11
43
|
export function wireLifecycle(ctx, gate, memory, settings, config) {
|
|
12
44
|
const disposers = [];
|
|
13
45
|
const wrap = (label, fn) => {
|
|
@@ -23,6 +55,18 @@ export function wireLifecycle(ctx, gate, memory, settings, config) {
|
|
|
23
55
|
// 清掉上次的诊断残留,保证每次重启后诊断只反映本轮真实情况
|
|
24
56
|
memory.kvSet('dbg.first_event_sample', '');
|
|
25
57
|
memory.kvSet('dbg.event_no_session', '');
|
|
58
|
+
memory.kvSet('dbg.event_stats', '');
|
|
59
|
+
// ⚠️ P5①(蓝队 2026-10-04):上面三家清了,`dbg.diff_stats` 却漏了 —— 于是差分路的读数会
|
|
60
|
+
// **跨重启残留**:新进程还没跑过任何一轮差分,诊断里却躺着一个旧时间戳的"本轮画像",
|
|
61
|
+
// 看起来像"刚跑过、结果是这样"。它现在和另外三家同批清。
|
|
62
|
+
memory.kvSet('dbg.diff_stats', '');
|
|
63
|
+
// ⚠️ P5③(同一处):`evStats` 是**模块级**对象,只在模块加载时归零 —— 同一进程里若二次
|
|
64
|
+
// `wireLifecycle`(换配置重载插件),计数会在上一轮的基数上继续累加,而 kv 里写的是"本轮"。
|
|
65
|
+
// 归零点放到这里,与 kv 的清理**同一时点**,两者才同义。
|
|
66
|
+
evStats.seen = 0; evStats.kept = 0; evStats.dropType = 0; evStats.dropSource = 0;
|
|
67
|
+
evStats.dropEmpty = 0; evStats.dropNoSession = 0; evStats.dropSubagent = 0;
|
|
68
|
+
evStats.types = {}; evStats.lastSid = '';
|
|
69
|
+
evStatsDirty = 0; evStatsAt = 0;
|
|
26
70
|
memory.kvSet('lifecycle.wired', '1');
|
|
27
71
|
|
|
28
72
|
const inTrackedWorkspace = () => {
|
|
@@ -141,6 +185,12 @@ export function wireLifecycle(ctx, gate, memory, settings, config) {
|
|
|
141
185
|
disposers.push(
|
|
142
186
|
ctx.on('session/event', wrap('event-capture', (session, event) => {
|
|
143
187
|
const type = event?.type;
|
|
188
|
+
// 丢弃计数(2026-10-04,P1 第三件):每个早返回都记一笔 —— 这些 return 以前是静默的。
|
|
189
|
+
evStats.seen += 1;
|
|
190
|
+
if (type) {
|
|
191
|
+
evStats.types[type] = (evStats.types[type] || 0) + 1;
|
|
192
|
+
evStats.lastSid = String(session?.id ?? session?.header?.id ?? evStats.lastSid);
|
|
193
|
+
}
|
|
144
194
|
if (!event) {
|
|
145
195
|
if (!memory.kvGet('dbg.first_event_sample')) {
|
|
146
196
|
try {
|
|
@@ -149,6 +199,7 @@ export function wireLifecycle(ctx, gate, memory, settings, config) {
|
|
|
149
199
|
}).slice(0, 300));
|
|
150
200
|
} catch {}
|
|
151
201
|
}
|
|
202
|
+
flushEventStats(memory, true);
|
|
152
203
|
return;
|
|
153
204
|
}
|
|
154
205
|
// U7(2026-10-01):feedback/* 是平台消息反馈事件 —— 早先在这里被一并丢掉,
|
|
@@ -156,25 +207,27 @@ export function wireLifecycle(ctx, gate, memory, settings, config) {
|
|
|
156
207
|
// registerFeedbackListener 处理(它订阅同一个 session/event);本 handler 自己
|
|
157
208
|
// 只用 user/assistant 消息,下面照旧过滤,互不干扰。
|
|
158
209
|
const isFeedback = type === 'feedback/message-put' || type === 'feedback/message-delete';
|
|
159
|
-
if (!isFeedback && type !== 'user/message' && type !== 'assistant/message') return;
|
|
160
|
-
if (isFeedback) return; // 放行到下游订阅后,本 handler 不再往下走(免得写 dbg 诊断噪声)
|
|
210
|
+
if (!isFeedback && type !== 'user/message' && type !== 'assistant/message') { evStats.dropType += 1; flushEventStats(memory); return; }
|
|
211
|
+
if (isFeedback) { flushEventStats(memory); return; } // 放行到下游订阅后,本 handler 不再往下走(免得写 dbg 诊断噪声)
|
|
161
212
|
// 只留真人消息(排除 plugin/skill-catalog/goal 注入)
|
|
162
|
-
if (type === 'user/message' && messageSourceKind(event) !== 'user') return;
|
|
213
|
+
if (type === 'user/message' && messageSourceKind(event) !== 'user') { evStats.dropSource += 1; flushEventStats(memory); return; }
|
|
163
214
|
const sessionId = pick(
|
|
164
215
|
() => session?.id,
|
|
165
216
|
() => session?.header?.id,
|
|
166
217
|
() => session?.sessionId,
|
|
167
218
|
);
|
|
168
219
|
if (!sessionId) {
|
|
220
|
+
evStats.dropNoSession += 1;
|
|
169
221
|
if (!memory.kvGet('dbg.event_no_session')) {
|
|
170
222
|
memory.kvSet('dbg.event_no_session', JSON.stringify({ type, eventKeys: Object.keys(event).slice(0, 10) }).slice(0, 300));
|
|
171
223
|
}
|
|
224
|
+
flushEventStats(memory, true);
|
|
172
225
|
return;
|
|
173
226
|
}
|
|
174
227
|
const header = pick(() => session?.header, () => session?.headerMeta);
|
|
175
|
-
if (header && !isMemoryEligibleHeader(header)) return; // 子代理/嵌套不入库(D9-a:分叉会话入枝)
|
|
228
|
+
if (header && !isMemoryEligibleHeader(header)) { evStats.dropSubagent += 1; flushEventStats(memory); return; } // 子代理/嵌套不入库(D9-a:分叉会话入枝)
|
|
176
229
|
const text = extractMessageText(event);
|
|
177
|
-
if (!text) return;
|
|
230
|
+
if (!text) { evStats.dropEmpty += 1; flushEventStats(memory); return; }
|
|
178
231
|
const seq = Number(event?.seq ?? (memory.rawTurnCount(sessionId) + 1));
|
|
179
232
|
const time = Number(event?.time);
|
|
180
233
|
const isUser = type.startsWith('user');
|
|
@@ -188,6 +241,8 @@ export function wireLifecycle(ctx, gate, memory, settings, config) {
|
|
|
188
241
|
// 不靠"id 里有没有冒号"推断:将来若宿主会话 id 形态变了(带冒号/带前缀),这一行仍然如实可读。
|
|
189
242
|
source: RAW_SOURCE_DSH,
|
|
190
243
|
});
|
|
244
|
+
evStats.kept += 1;
|
|
245
|
+
flushEventStats(memory);
|
|
191
246
|
if (isUser) stampModeIfFirstInput(memory, settings, String(sessionId));
|
|
192
247
|
})),
|
|
193
248
|
);
|
|
@@ -333,13 +388,14 @@ export function captureSessionDiff(memory, session, settings = null) {
|
|
|
333
388
|
let added = 0;
|
|
334
389
|
let userAdded = 0;
|
|
335
390
|
const typeCount = {};
|
|
391
|
+
const drop = { type: 0, source: 0, empty: 0 }; // P1 第三件:丢弃计数(以前这三处 continue 全是静默的)
|
|
336
392
|
const msgLike = [];
|
|
337
393
|
for (const ev of events) {
|
|
338
394
|
const s = Number(ev?.seq ?? 0);
|
|
339
395
|
if (s > lastSeq) lastSeq = s;
|
|
340
396
|
if (!s || s <= wm) continue;
|
|
341
397
|
const type = ev?.type;
|
|
342
|
-
if (!type) continue;
|
|
398
|
+
if (!type) { drop.type += 1; continue; }
|
|
343
399
|
typeCount[type] = (typeCount[type] || 0) + 1;
|
|
344
400
|
if (String(type).includes('message') && msgLike.length < 3) {
|
|
345
401
|
const data = ev?.data ?? {};
|
|
@@ -349,11 +405,11 @@ export function captureSessionDiff(memory, session, settings = null) {
|
|
|
349
405
|
msgKeys: data?.message && typeof data.message === 'object' ? Object.keys(data.message).slice(0, 8) : null,
|
|
350
406
|
});
|
|
351
407
|
}
|
|
352
|
-
if (type !== 'user/message' && type !== 'assistant/message') continue;
|
|
408
|
+
if (type !== 'user/message' && type !== 'assistant/message') { drop.type += 1; continue; }
|
|
353
409
|
// 只留真人消息
|
|
354
|
-
if (type === 'user/message' && messageSourceKind(ev) !== 'user') continue;
|
|
410
|
+
if (type === 'user/message' && messageSourceKind(ev) !== 'user') { drop.source += 1; continue; }
|
|
355
411
|
const text = extractMessageText(ev);
|
|
356
|
-
if (!text) continue;
|
|
412
|
+
if (!text) { drop.empty += 1; continue; }
|
|
357
413
|
const time = Number(ev?.time);
|
|
358
414
|
memory.appendRawTurn(String(sid), {
|
|
359
415
|
seq: s,
|
|
@@ -369,6 +425,16 @@ export function captureSessionDiff(memory, session, settings = null) {
|
|
|
369
425
|
}
|
|
370
426
|
if (lastSeq > wm) memory.kvSet(key, String(lastSeq));
|
|
371
427
|
if (userAdded > 0 && settings) stampModeIfFirstInput(memory, settings, sid);
|
|
428
|
+
// P1 第三件(2026-10-04):把本轮的**丢弃画像**记下来 —— 以前只在 `added === 0` 且第一次时才写一条
|
|
429
|
+
// `dbg.diff_types`,于是"正常路径上丢了多少"完全不可见(2026-10-04 的 P1 侦察就是卡在这一条上)。
|
|
430
|
+
// 现在每轮都覆盖写,并带上水位与已写/已丢的对照。
|
|
431
|
+
try {
|
|
432
|
+
memory.kvSet('dbg.diff_stats', JSON.stringify({
|
|
433
|
+
at: new Date().toISOString(), sid: String(sid), wm, lastSeq,
|
|
434
|
+
scanned: typeCount ? Object.values(typeCount).reduce((a, b) => a + b, 0) : 0,
|
|
435
|
+
added, userAdded, dropped: drop, types: Object.fromEntries(Object.entries(typeCount).sort((a, b) => b[1] - a[1]).slice(0, 12)),
|
|
436
|
+
}).slice(0, 1500));
|
|
437
|
+
} catch { /* 诊断写不进去不影响主流程 */ }
|
|
372
438
|
if (added === 0 && !memory.kvGet('dbg.diff_types') && Object.keys(typeCount).length) {
|
|
373
439
|
try {
|
|
374
440
|
memory.kvSet('dbg.diff_types', JSON.stringify({ types: typeCount, msgLike }).slice(0, 900));
|