@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.
- package/ACCESS-DESIGN.md +4 -3
- package/CHANGELOG.md +150 -9
- package/DESIGN.md +32 -11
- package/README.md +194 -10
- package/lib/audit/access-log.js +46 -12
- package/lib/audit/index.js +2 -2
- package/lib/audit/patch-spec.js +1 -1
- package/lib/client.js +319 -69
- package/lib/host/api.js +602 -60
- package/lib/host/archive.js +15 -9
- package/lib/host/autotree.js +2 -2
- package/lib/host/backfill.js +8 -2
- package/lib/host/clock-skill.js +1 -1
- package/lib/host/deep-index-skill.js +111 -0
- package/lib/host/feedback.js +112 -2
- package/lib/host/freeze.js +52 -1
- package/lib/host/guard.js +38 -1
- package/lib/host/habit-gen.js +12 -4
- package/lib/host/import-file.js +62 -6
- package/lib/host/import-log.js +80 -2
- 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 +483 -48
- package/lib/host/mode-service.js +85 -0
- package/lib/host/mode.js +29 -9
- package/lib/host/persona.js +2 -2
- package/lib/host/rawlog.js +317 -0
- package/lib/host/recall-search.js +210 -0
- package/lib/host/retitle.js +11 -5
- package/lib/host/session-line.js +46 -6
- package/lib/host/snapshot.js +65 -13
- package/lib/host/summarizer.js +164 -2
- package/lib/host/surgery.js +600 -0
- package/lib/host/tools.js +339 -15
- package/lib/host/tree-ops.js +13 -7
- package/lib/host/util.js +1 -1
- package/lib/index.js +58 -31
- package/package.json +6 -2
- package/tools/apply-access-log-patch.mjs +1 -1
package/lib/host/lifecycle.js
CHANGED
|
@@ -1,13 +1,45 @@
|
|
|
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';
|
|
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
|
-
|
|
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));
|