@nightpainters/dsh-ling 1.5.3 → 1.6.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.
@@ -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,7 @@ 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', '');
26
59
  memory.kvSet('lifecycle.wired', '1');
27
60
 
28
61
  const inTrackedWorkspace = () => {
@@ -141,6 +174,12 @@ export function wireLifecycle(ctx, gate, memory, settings, config) {
141
174
  disposers.push(
142
175
  ctx.on('session/event', wrap('event-capture', (session, event) => {
143
176
  const type = event?.type;
177
+ // 丢弃计数(2026-10-04,P1 第三件):每个早返回都记一笔 —— 这些 return 以前是静默的。
178
+ evStats.seen += 1;
179
+ if (type) {
180
+ evStats.types[type] = (evStats.types[type] || 0) + 1;
181
+ evStats.lastSid = String(session?.id ?? session?.header?.id ?? evStats.lastSid);
182
+ }
144
183
  if (!event) {
145
184
  if (!memory.kvGet('dbg.first_event_sample')) {
146
185
  try {
@@ -149,6 +188,7 @@ export function wireLifecycle(ctx, gate, memory, settings, config) {
149
188
  }).slice(0, 300));
150
189
  } catch {}
151
190
  }
191
+ flushEventStats(memory, true);
152
192
  return;
153
193
  }
154
194
  // U7(2026-10-01):feedback/* 是平台消息反馈事件 —— 早先在这里被一并丢掉,
@@ -156,25 +196,27 @@ export function wireLifecycle(ctx, gate, memory, settings, config) {
156
196
  // registerFeedbackListener 处理(它订阅同一个 session/event);本 handler 自己
157
197
  // 只用 user/assistant 消息,下面照旧过滤,互不干扰。
158
198
  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 诊断噪声)
199
+ if (!isFeedback && type !== 'user/message' && type !== 'assistant/message') { evStats.dropType += 1; flushEventStats(memory); return; }
200
+ if (isFeedback) { flushEventStats(memory); return; } // 放行到下游订阅后,本 handler 不再往下走(免得写 dbg 诊断噪声)
161
201
  // 只留真人消息(排除 plugin/skill-catalog/goal 注入)
162
- if (type === 'user/message' && messageSourceKind(event) !== 'user') return;
202
+ if (type === 'user/message' && messageSourceKind(event) !== 'user') { evStats.dropSource += 1; flushEventStats(memory); return; }
163
203
  const sessionId = pick(
164
204
  () => session?.id,
165
205
  () => session?.header?.id,
166
206
  () => session?.sessionId,
167
207
  );
168
208
  if (!sessionId) {
209
+ evStats.dropNoSession += 1;
169
210
  if (!memory.kvGet('dbg.event_no_session')) {
170
211
  memory.kvSet('dbg.event_no_session', JSON.stringify({ type, eventKeys: Object.keys(event).slice(0, 10) }).slice(0, 300));
171
212
  }
213
+ flushEventStats(memory, true);
172
214
  return;
173
215
  }
174
216
  const header = pick(() => session?.header, () => session?.headerMeta);
175
- if (header && !isMemoryEligibleHeader(header)) return; // 子代理/嵌套不入库(D9-a:分叉会话入枝)
217
+ if (header && !isMemoryEligibleHeader(header)) { evStats.dropSubagent += 1; flushEventStats(memory); return; } // 子代理/嵌套不入库(D9-a:分叉会话入枝)
176
218
  const text = extractMessageText(event);
177
- if (!text) return;
219
+ if (!text) { evStats.dropEmpty += 1; flushEventStats(memory); return; }
178
220
  const seq = Number(event?.seq ?? (memory.rawTurnCount(sessionId) + 1));
179
221
  const time = Number(event?.time);
180
222
  const isUser = type.startsWith('user');
@@ -188,6 +230,8 @@ export function wireLifecycle(ctx, gate, memory, settings, config) {
188
230
  // 不靠"id 里有没有冒号"推断:将来若宿主会话 id 形态变了(带冒号/带前缀),这一行仍然如实可读。
189
231
  source: RAW_SOURCE_DSH,
190
232
  });
233
+ evStats.kept += 1;
234
+ flushEventStats(memory);
191
235
  if (isUser) stampModeIfFirstInput(memory, settings, String(sessionId));
192
236
  })),
193
237
  );
@@ -333,13 +377,14 @@ export function captureSessionDiff(memory, session, settings = null) {
333
377
  let added = 0;
334
378
  let userAdded = 0;
335
379
  const typeCount = {};
380
+ const drop = { type: 0, source: 0, empty: 0 }; // P1 第三件:丢弃计数(以前这三处 continue 全是静默的)
336
381
  const msgLike = [];
337
382
  for (const ev of events) {
338
383
  const s = Number(ev?.seq ?? 0);
339
384
  if (s > lastSeq) lastSeq = s;
340
385
  if (!s || s <= wm) continue;
341
386
  const type = ev?.type;
342
- if (!type) continue;
387
+ if (!type) { drop.type += 1; continue; }
343
388
  typeCount[type] = (typeCount[type] || 0) + 1;
344
389
  if (String(type).includes('message') && msgLike.length < 3) {
345
390
  const data = ev?.data ?? {};
@@ -349,11 +394,11 @@ export function captureSessionDiff(memory, session, settings = null) {
349
394
  msgKeys: data?.message && typeof data.message === 'object' ? Object.keys(data.message).slice(0, 8) : null,
350
395
  });
351
396
  }
352
- if (type !== 'user/message' && type !== 'assistant/message') continue;
397
+ if (type !== 'user/message' && type !== 'assistant/message') { drop.type += 1; continue; }
353
398
  // 只留真人消息
354
- if (type === 'user/message' && messageSourceKind(ev) !== 'user') continue;
399
+ if (type === 'user/message' && messageSourceKind(ev) !== 'user') { drop.source += 1; continue; }
355
400
  const text = extractMessageText(ev);
356
- if (!text) continue;
401
+ if (!text) { drop.empty += 1; continue; }
357
402
  const time = Number(ev?.time);
358
403
  memory.appendRawTurn(String(sid), {
359
404
  seq: s,
@@ -369,6 +414,16 @@ export function captureSessionDiff(memory, session, settings = null) {
369
414
  }
370
415
  if (lastSeq > wm) memory.kvSet(key, String(lastSeq));
371
416
  if (userAdded > 0 && settings) stampModeIfFirstInput(memory, settings, sid);
417
+ // P1 第三件(2026-10-04):把本轮的**丢弃画像**记下来 —— 以前只在 `added === 0` 且第一次时才写一条
418
+ // `dbg.diff_types`,于是"正常路径上丢了多少"完全不可见(2026-10-04 的 P1 侦察就是卡在这一条上)。
419
+ // 现在每轮都覆盖写,并带上水位与已写/已丢的对照。
420
+ try {
421
+ memory.kvSet('dbg.diff_stats', JSON.stringify({
422
+ at: new Date().toISOString(), sid: String(sid), wm, lastSeq,
423
+ scanned: typeCount ? Object.values(typeCount).reduce((a, b) => a + b, 0) : 0,
424
+ added, userAdded, dropped: drop, types: Object.fromEntries(Object.entries(typeCount).sort((a, b) => b[1] - a[1]).slice(0, 12)),
425
+ }).slice(0, 1500));
426
+ } catch { /* 诊断写不进去不影响主流程 */ }
372
427
  if (added === 0 && !memory.kvGet('dbg.diff_types') && Object.keys(typeCount).length) {
373
428
  try {
374
429
  memory.kvSet('dbg.diff_types', JSON.stringify({ types: typeCount, msgLike }).slice(0, 900));