@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.
@@ -1,4 +1,4 @@
1
- // dsh-ling host — lifecycle wiring (REPORT-02 / DESIGN §4.4):
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));