@nightpainters/dsh-ling 1.5.2 → 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,13 +1,45 @@
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';
5
- import { TRUNK_ID } from './memory.js';
5
+ import { TRUNK_ID, RAW_SOURCE_DSH } from './memory.js';
6
6
  import { freezeSnapshotForSession, invalidateSession, refreshIdleSnapshot } from './inject.js';
7
7
  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,26 +188,35 @@ 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
- if (type !== 'user/message' && type !== 'assistant/message') return;
194
+ // U7(2026-10-01):feedback/* 是平台消息反馈事件 —— 早先在这里被一并丢掉,
195
+ // 于是活会话里"当场点赞/点踩"根本没有下游。放行它们,交 feedback.js 的
196
+ // registerFeedbackListener 处理(它订阅同一个 session/event);本 handler 自己
197
+ // 只用 user/assistant 消息,下面照旧过滤,互不干扰。
198
+ const isFeedback = type === 'feedback/message-put' || type === 'feedback/message-delete';
199
+ if (!isFeedback && type !== 'user/message' && type !== 'assistant/message') { evStats.dropType += 1; flushEventStats(memory); return; }
200
+ if (isFeedback) { flushEventStats(memory); return; } // 放行到下游订阅后,本 handler 不再往下走(免得写 dbg 诊断噪声)
155
201
  // 只留真人消息(排除 plugin/skill-catalog/goal 注入)
156
- if (type === 'user/message' && messageSourceKind(event) !== 'user') return;
202
+ if (type === 'user/message' && messageSourceKind(event) !== 'user') { evStats.dropSource += 1; flushEventStats(memory); return; }
157
203
  const sessionId = pick(
158
204
  () => session?.id,
159
205
  () => session?.header?.id,
160
206
  () => session?.sessionId,
161
207
  );
162
208
  if (!sessionId) {
209
+ evStats.dropNoSession += 1;
163
210
  if (!memory.kvGet('dbg.event_no_session')) {
164
211
  memory.kvSet('dbg.event_no_session', JSON.stringify({ type, eventKeys: Object.keys(event).slice(0, 10) }).slice(0, 300));
165
212
  }
213
+ flushEventStats(memory, true);
166
214
  return;
167
215
  }
168
216
  const header = pick(() => session?.header, () => session?.headerMeta);
169
- if (header && !isMemoryEligibleHeader(header)) return; // 子代理/嵌套不入库(D9-a:分叉会话入枝)
217
+ if (header && !isMemoryEligibleHeader(header)) { evStats.dropSubagent += 1; flushEventStats(memory); return; } // 子代理/嵌套不入库(D9-a:分叉会话入枝)
170
218
  const text = extractMessageText(event);
171
- if (!text) return;
219
+ if (!text) { evStats.dropEmpty += 1; flushEventStats(memory); return; }
172
220
  const seq = Number(event?.seq ?? (memory.rawTurnCount(sessionId) + 1));
173
221
  const time = Number(event?.time);
174
222
  const isUser = type.startsWith('user');
@@ -178,7 +226,12 @@ export function wireLifecycle(ctx, gate, memory, settings, config) {
178
226
  ts: time && !Number.isNaN(time) ? new Date(time).toISOString() : null,
179
227
  model: null,
180
228
  text,
229
+ // v14(2026-10-01):这里是**唯一**"主人自己的会话"写入点 ⇒ 来源当场记成 'dsh'。
230
+ // 不靠"id 里有没有冒号"推断:将来若宿主会话 id 形态变了(带冒号/带前缀),这一行仍然如实可读。
231
+ source: RAW_SOURCE_DSH,
181
232
  });
233
+ evStats.kept += 1;
234
+ flushEventStats(memory);
182
235
  if (isUser) stampModeIfFirstInput(memory, settings, String(sessionId));
183
236
  })),
184
237
  );
@@ -324,13 +377,14 @@ export function captureSessionDiff(memory, session, settings = null) {
324
377
  let added = 0;
325
378
  let userAdded = 0;
326
379
  const typeCount = {};
380
+ const drop = { type: 0, source: 0, empty: 0 }; // P1 第三件:丢弃计数(以前这三处 continue 全是静默的)
327
381
  const msgLike = [];
328
382
  for (const ev of events) {
329
383
  const s = Number(ev?.seq ?? 0);
330
384
  if (s > lastSeq) lastSeq = s;
331
385
  if (!s || s <= wm) continue;
332
386
  const type = ev?.type;
333
- if (!type) continue;
387
+ if (!type) { drop.type += 1; continue; }
334
388
  typeCount[type] = (typeCount[type] || 0) + 1;
335
389
  if (String(type).includes('message') && msgLike.length < 3) {
336
390
  const data = ev?.data ?? {};
@@ -340,11 +394,11 @@ export function captureSessionDiff(memory, session, settings = null) {
340
394
  msgKeys: data?.message && typeof data.message === 'object' ? Object.keys(data.message).slice(0, 8) : null,
341
395
  });
342
396
  }
343
- if (type !== 'user/message' && type !== 'assistant/message') continue;
397
+ if (type !== 'user/message' && type !== 'assistant/message') { drop.type += 1; continue; }
344
398
  // 只留真人消息
345
- if (type === 'user/message' && messageSourceKind(ev) !== 'user') continue;
399
+ if (type === 'user/message' && messageSourceKind(ev) !== 'user') { drop.source += 1; continue; }
346
400
  const text = extractMessageText(ev);
347
- if (!text) continue;
401
+ if (!text) { drop.empty += 1; continue; }
348
402
  const time = Number(ev?.time);
349
403
  memory.appendRawTurn(String(sid), {
350
404
  seq: s,
@@ -352,12 +406,24 @@ export function captureSessionDiff(memory, session, settings = null) {
352
406
  ts: time && !Number.isNaN(time) ? new Date(time).toISOString() : null,
353
407
  model: null,
354
408
  text,
409
+ // v14:与 session/event 那条路同一来源口径(同一条链的两个入口,口径不许分叉)
410
+ source: RAW_SOURCE_DSH,
355
411
  });
356
412
  added += 1;
357
413
  if (type.startsWith('user')) userAdded += 1;
358
414
  }
359
415
  if (lastSeq > wm) memory.kvSet(key, String(lastSeq));
360
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 { /* 诊断写不进去不影响主流程 */ }
361
427
  if (added === 0 && !memory.kvGet('dbg.diff_types') && Object.keys(typeCount).length) {
362
428
  try {
363
429
  memory.kvSet('dbg.diff_types', JSON.stringify({ types: typeCount, msgLike }).slice(0, 900));