@nightpainters/dsh-ling 1.5.1 → 1.5.3
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 +10 -7
- package/CHANGELOG.md +140 -3
- package/DESIGN.md +162 -18
- package/README.md +66 -32
- package/lib/audit/access-log.js +181 -25
- package/lib/audit/index.js +45 -3
- package/lib/audit/patch-spec.js +1 -1
- package/lib/client.js +1183 -88
- package/lib/host/api.js +716 -83
- package/lib/host/backfill.js +8 -2
- package/lib/host/feedback.js +112 -2
- package/lib/host/freeze.js +52 -1
- package/lib/host/genesis.js +27 -5
- package/lib/host/guard.js +39 -2
- 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 +49 -12
- package/lib/host/l1.js +11 -4
- package/lib/host/lifecycle.js +13 -2
- package/lib/host/memory.js +425 -33
- package/lib/host/mode-service.js +85 -0
- package/lib/host/mode.js +29 -9
- package/lib/host/persona.js +38 -7
- package/lib/host/retitle.js +6 -0
- package/lib/host/session-line.js +688 -0
- package/lib/host/snapshot.js +48 -7
- package/lib/host/summarizer.js +159 -3
- package/lib/host/surgery.js +600 -0
- package/lib/host/tools.js +12 -1
- package/lib/host/tree-ops.js +1 -1
- package/lib/index.js +22 -29
- package/package.json +51 -4
- package/tools/apply-access-log-patch.mjs +1 -1
package/lib/host/inject.js
CHANGED
|
@@ -23,7 +23,8 @@ function agentOf(assembly) {
|
|
|
23
23
|
return pick(() => assembly?.agent, () => assembly?.scope?.agent, () => assembly?.ctx?.agent);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
/** 导出给 `session-line.js`(E0)复用同一套取值口径 —— 只加 `export`,逻辑逐字未动。 */
|
|
27
|
+
export function sessionIdOfAgent(agent) {
|
|
27
28
|
return pick(
|
|
28
29
|
() => agent?.session?.id,
|
|
29
30
|
() => agent?.session?.header?.id,
|
|
@@ -35,7 +36,7 @@ function sessionIdOfAgent(agent) {
|
|
|
35
36
|
/** 会话种类(D9-a):'top' | 'fork' | 'subagent' | 'unknown'。
|
|
36
37
|
* 旧版把 fork 与 subagent 一并当 child 丢弃 ⇒ 分叉会话记忆真空;
|
|
37
38
|
* 现在**分叉会话照常注入**(读血缘链),只有子代理与"取不到 header"仍保守拒绝。 */
|
|
38
|
-
function kindOfAgent(agent) {
|
|
39
|
+
export function kindOfAgent(agent) {
|
|
39
40
|
const header = pick(
|
|
40
41
|
() => agent?.session?.header,
|
|
41
42
|
() => agent?.header,
|
|
@@ -43,21 +44,50 @@ function kindOfAgent(agent) {
|
|
|
43
44
|
return sessionKind(header);
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
/** 能否注入(D9-a):顶层与分叉会话可以,子代理/未知不可以。
|
|
47
|
-
|
|
47
|
+
/** 能否注入(D9-a):顶层与分叉会话可以,子代理/未知不可以。
|
|
48
|
+
* ⚠️ 入参是**种类字符串**(不是 agent):E0 的 `agent/pre-step` 是全局注册,
|
|
49
|
+
* 按 `dsh-scope/lib/invariant.js:17` 会收到**所有 agent(含子代理)**,故必须自己过这道闸。 */
|
|
50
|
+
export function canInject(kind) {
|
|
48
51
|
return kind === 'top' || kind === 'fork';
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
/** B#9(2026-09-22 审计):消费注入感知水位 —— **只在真正要把文本送进模型时调用**。
|
|
52
55
|
* 渲染(buildSnapshotText)本身不推进:它也会被"只重建、未开口"的路径调用
|
|
53
56
|
* (invalidateSession / refreshIdleSnapshot),在那里推进会把主人刚改的东西
|
|
54
|
-
* 标记成已读,等真正开口时反而看不到。水位按会话、单调不回退。
|
|
55
|
-
|
|
57
|
+
* 标记成已读,等真正开口时反而看不到。水位按会话、单调不回退。
|
|
58
|
+
*
|
|
59
|
+
* ── B3(1.5.2):入参从一个 maxId 拆成三个,各管一件事 ────────────────────────
|
|
60
|
+
* · `maxId` branch_log **全表最大 id** ⇒ 只喂 `branch_log_wm.last`。
|
|
61
|
+
* 它跟"示人没示人"无关:一次只渲染 5 行也照旧推到全表最新,这是**有意的**
|
|
62
|
+
* (复盘模式的退出判据,消费点 memory.branchLogPending,本版逐行未动)。
|
|
63
|
+
* · `shownMaxId` 本轮**真正渲染过**的最大 id ⇒ 推按会话水位 + 全局 announced。
|
|
64
|
+
* B3-2 的病根就是这里以前喂的是 maxId:一次播报把中间 114 条标成已读。
|
|
65
|
+
* · `retiredMaxId` 本轮按时间窗"计数式处置"的最大 id(§2 老改动只计数不展开)⇒ 推 retired。
|
|
66
|
+
* 它**不算已示人**,只让同一批老账不被每轮重算(否则水位永不前进)。
|
|
67
|
+
* `announceOn=false`(§3 开关关闭)⇒ 只推 `.last`,两个"已示人/已处置"水位一律不动。
|
|
68
|
+
* **开关只管"渲染",不管"交代记账"**:`.last` 是**复盘模式的退出判据**,不是"已示人"判据 ——
|
|
69
|
+
* 关了它,侧栏会永远显示"还有 N 笔没说给器灵"、复盘模式退不出去;而 `.last` **不参与播报下界**
|
|
70
|
+
* (下界只取 会话/announced/retired 三者更大者)⇒ 它照走**不会**造成"关着开着丢一段内容":
|
|
71
|
+
* 那条改动仍在候选里,开关一开照样播。
|
|
72
|
+
* 回归:tests/tree.test.mjs 第 9-5 条(关闭 ⇒ 不播报、两个新水位不动、`.last` 照走、重开仍看得见)。
|
|
73
|
+
* 字面量与 memory.js 的 BRANCH_LOG_WM_ANNOUNCED / BRANCH_LOG_WM_RETIRED 一致。 */
|
|
74
|
+
function advanceLogWatermark(memory, sessionId, maxId, shownMaxId, retiredMaxId, announceOn = true) {
|
|
56
75
|
const v = Number(maxId);
|
|
57
76
|
if (!sessionId || !Number.isFinite(v) || v <= 0) return;
|
|
77
|
+
// 旧快照(1.5.1 冻结在内存里的)没有 shownMaxId ⇒ 按 0 处理:**宁可下一轮重播一遍,
|
|
78
|
+
// 也不把没示人的内容标成已读**(与 B3 的取向一致)。`.last` 不受影响。
|
|
79
|
+
const seen = announceOn ? Number(shownMaxId) || 0 : 0;
|
|
80
|
+
const retired = announceOn ? Number(retiredMaxId) || 0 : 0;
|
|
58
81
|
try {
|
|
59
|
-
|
|
60
|
-
|
|
82
|
+
if (seen > 0) {
|
|
83
|
+
const k = 'branch_log_wm.' + String(sessionId);
|
|
84
|
+
if (seen > Number(memory.kvGet(k) || 0)) memory.kvSet(k, String(seen));
|
|
85
|
+
// 全局"已示人"边界(B3-1):别的会话已经看过的,新会话不再重播。
|
|
86
|
+
if (seen > Number(memory.kvGet('branch_log_wm.announced') || 0)) memory.kvSet('branch_log_wm.announced', String(seen));
|
|
87
|
+
}
|
|
88
|
+
if (retired > 0 && retired > Number(memory.kvGet('branch_log_wm.retired') || 0)) {
|
|
89
|
+
memory.kvSet('branch_log_wm.retired', String(retired));
|
|
90
|
+
}
|
|
61
91
|
// 复盘守门(2026-09-22 用户:「动记忆树不能是随意的,应该是慎重严肃的」):
|
|
62
92
|
// 另记一个**全局最后同步**水位,回答"自器灵上次开口以来累积了多少笔改动还没交代"
|
|
63
93
|
// (memory.branchLogPending 读它)。与会话水位互补 —— 会话水位管"这个会话看到什么",
|
|
@@ -151,6 +181,13 @@ export function renderMemorySection(assembly, gate, memory, settings) {
|
|
|
151
181
|
return (body ? body + '\n\n' : '') + neutralizeMustache(anchor);
|
|
152
182
|
};
|
|
153
183
|
const snap = gate.snapshotOf(sessionId);
|
|
184
|
+
// §3 独立开关(1.5.2):只管"树改动播报"。关闭时**不推进任何"已示人"水位** ——
|
|
185
|
+
// 否则关着的那段时间里,这段内容会被标成已读,再打开就永远看不到它了。
|
|
186
|
+
// ⚠️ 例外且**有意**:`branch_log_wm.last`(复盘退出判据,侧栏 GET /branch-log/pending 拦门)
|
|
187
|
+
// 照旧推进 —— 它记的是"还没跟器灵交代过多少笔",不是"示人没示人",停推进会卡死复盘退出。
|
|
188
|
+
const announceOn = (() => {
|
|
189
|
+
try { return settings.get()?.memory?.branchLogEnabled !== false; } catch { return true; }
|
|
190
|
+
})();
|
|
154
191
|
if (!snap) {
|
|
155
192
|
// 首轮组装可能先于/晚于 session-start 事件:空闲时补建,运行时不建(D4)
|
|
156
193
|
if (gate.isRunning(sessionId)) return withAnchor('');
|
|
@@ -158,12 +195,12 @@ export function renderMemorySection(assembly, gate, memory, settings) {
|
|
|
158
195
|
const kw = keywordsFrom(firstUserText(agent));
|
|
159
196
|
const built = buildSnapshotText(null, memory, settings, sessionId, { keywords: kw, bumpHits: true });
|
|
160
197
|
gate.markSnap(sessionId, { ...built, sessionId, mode, keywords: kw });
|
|
161
|
-
advanceLogWatermark(memory, sessionId, built.logMaxId); // B#9:定稿即消费
|
|
198
|
+
advanceLogWatermark(memory, sessionId, built.logMaxId, built.logShownMaxId, built.logRetiredMaxId, announceOn); // B#9:定稿即消费
|
|
162
199
|
return withAnchor(built.text);
|
|
163
200
|
}
|
|
164
201
|
// 冻结:running 一律返回既有快照(即使 stale);时间锚仍实时
|
|
165
202
|
if (gate.isRunning(sessionId)) {
|
|
166
|
-
advanceLogWatermark(memory, sessionId, snap.logMaxId); // B#9:冻结快照投递即消费
|
|
203
|
+
advanceLogWatermark(memory, sessionId, snap.logMaxId, snap.logShownMaxId, snap.logRetiredMaxId, announceOn); // B#9:冻结快照投递即消费
|
|
167
204
|
return withAnchor(snap.text || '');
|
|
168
205
|
}
|
|
169
206
|
// idle + stale → 懒重建(一次性失效);重建必须带上首次定稿时的关键词,否则排序口径会变
|
|
@@ -172,10 +209,10 @@ export function renderMemorySection(assembly, gate, memory, settings) {
|
|
|
172
209
|
const kw = Array.isArray(snap.keywords) ? snap.keywords : keywordsFrom(firstUserText(agent));
|
|
173
210
|
const built = buildSnapshotText(null, memory, settings, sessionId, { keywords: kw });
|
|
174
211
|
gate.markSnap(sessionId, { ...built, sessionId, mode, keywords: kw });
|
|
175
|
-
advanceLogWatermark(memory, sessionId, built.logMaxId); // B#9:重建后投递即消费
|
|
212
|
+
advanceLogWatermark(memory, sessionId, built.logMaxId, built.logShownMaxId, built.logRetiredMaxId, announceOn); // B#9:重建后投递即消费
|
|
176
213
|
return withAnchor(built.text);
|
|
177
214
|
}
|
|
178
|
-
advanceLogWatermark(memory, sessionId, snap.logMaxId); // B#9:命中缓存快照投递即消费
|
|
215
|
+
advanceLogWatermark(memory, sessionId, snap.logMaxId, snap.logShownMaxId, snap.logRetiredMaxId, announceOn); // B#9:命中缓存快照投递即消费
|
|
179
216
|
return withAnchor(snap.text || '');
|
|
180
217
|
} catch (e) {
|
|
181
218
|
console.debug('[dsh-ling] textForAssembly failed', e);
|
package/lib/host/l1.js
CHANGED
|
@@ -210,7 +210,7 @@ export function selectL1(store, opts) {
|
|
|
210
210
|
const items = [];
|
|
211
211
|
let totalChars = 0;
|
|
212
212
|
for (const c of chosen) {
|
|
213
|
-
const line = lineFor(c.row, { maxChars: o.maxLineChars, summaryChars: o.summaryChars });
|
|
213
|
+
const line = lineFor(c.row, { maxChars: o.maxLineChars, summaryChars: o.summaryChars, selfConvId: o.sessionId });
|
|
214
214
|
if (items.length >= max || totalChars + line.length > budget) break;
|
|
215
215
|
items.push({ ...c.row, score: +c.score.toFixed(3), lineage: +Number(c.lineage).toFixed(2), conf: +Number(c.conf ?? 1).toFixed(2), raw: +Number(c.raw).toFixed(3), kw: c.kw, rich: c.rich, line });
|
|
216
216
|
totalChars += line.length;
|
|
@@ -247,9 +247,11 @@ export function firstSentence(text, cap = SUMMARY_CHARS_DEFAULT) {
|
|
|
247
247
|
* 无摘要 → 只用标题(信息量优先,不截;仍受 maxChars 兜底)
|
|
248
248
|
* 摘要自带标题(概述器/深摘的常态)→ 不重复前缀,只留正文
|
|
249
249
|
* @param {object} row
|
|
250
|
-
* @param {{maxChars?:number, summaryChars?:number}} opts
|
|
250
|
+
* @param {{maxChars?:number, summaryChars?:number, selfConvId?:string}} opts
|
|
251
|
+
* `selfConvId`(E0,1.5.2):**本会话自己那一行**只留标题、不带摘要 ——
|
|
252
|
+
* 它的"进展"由活行(lib/host/session-line.js)承担;两边都印摘要 = 同一句话出现两遍。
|
|
251
253
|
*/
|
|
252
|
-
function lineFor(row, { maxChars = 0, summaryChars = SUMMARY_CHARS_DEFAULT } = {}) {
|
|
254
|
+
function lineFor(row, { maxChars = 0, summaryChars = SUMMARY_CHARS_DEFAULT, selfConvId = '' } = {}) {
|
|
253
255
|
const date = isoDate(row.updated_at || row.started_at);
|
|
254
256
|
const tags = Array.isArray(row.domain_tags) && row.domain_tags.length ? `[${row.domain_tags.slice(0, 2).join('/')}]` : `[${row.category}]`;
|
|
255
257
|
const title = String(row.title || '').trim();
|
|
@@ -258,7 +260,12 @@ function lineFor(row, { maxChars = 0, summaryChars = SUMMARY_CHARS_DEFAULT } = {
|
|
|
258
260
|
let body = summary;
|
|
259
261
|
if (title && body.startsWith(title)) body = body.slice(title.length).replace(/^[\s::—-]+/, '');
|
|
260
262
|
if (body === title) body = '';
|
|
261
|
-
|
|
263
|
+
// E0(1.5.2):本会话自己那一行 → 只要标题(摘要在活行里,不重复印)。
|
|
264
|
+
// 兜底:万一这行没有标题,仍退回摘要首句 —— 绝不让它渲染成"只有标签和来源"的空行。
|
|
265
|
+
const isSelf = !!selfConvId && String(row.conv_id || '') === String(selfConvId);
|
|
266
|
+
const text = isSelf
|
|
267
|
+
? (title || firstSentence(body, summaryChars))
|
|
268
|
+
: ((title && body) ? `${title} —— ${firstSentence(body, summaryChars)}` : (title || firstSentence(body, summaryChars)));
|
|
262
269
|
// 兜底硬顶:只对病态长标题生效(摘要侧已由 summaryChars 控住)
|
|
263
270
|
const cap = Number(maxChars) > 0 ? Math.max(60, Number(maxChars)) : 0;
|
|
264
271
|
let out = text;
|
package/lib/host/lifecycle.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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';
|
|
@@ -151,7 +151,13 @@ export function wireLifecycle(ctx, gate, memory, settings, config) {
|
|
|
151
151
|
}
|
|
152
152
|
return;
|
|
153
153
|
}
|
|
154
|
-
|
|
154
|
+
// U7(2026-10-01):feedback/* 是平台消息反馈事件 —— 早先在这里被一并丢掉,
|
|
155
|
+
// 于是活会话里"当场点赞/点踩"根本没有下游。放行它们,交 feedback.js 的
|
|
156
|
+
// registerFeedbackListener 处理(它订阅同一个 session/event);本 handler 自己
|
|
157
|
+
// 只用 user/assistant 消息,下面照旧过滤,互不干扰。
|
|
158
|
+
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 诊断噪声)
|
|
155
161
|
// 只留真人消息(排除 plugin/skill-catalog/goal 注入)
|
|
156
162
|
if (type === 'user/message' && messageSourceKind(event) !== 'user') return;
|
|
157
163
|
const sessionId = pick(
|
|
@@ -178,6 +184,9 @@ export function wireLifecycle(ctx, gate, memory, settings, config) {
|
|
|
178
184
|
ts: time && !Number.isNaN(time) ? new Date(time).toISOString() : null,
|
|
179
185
|
model: null,
|
|
180
186
|
text,
|
|
187
|
+
// v14(2026-10-01):这里是**唯一**"主人自己的会话"写入点 ⇒ 来源当场记成 'dsh'。
|
|
188
|
+
// 不靠"id 里有没有冒号"推断:将来若宿主会话 id 形态变了(带冒号/带前缀),这一行仍然如实可读。
|
|
189
|
+
source: RAW_SOURCE_DSH,
|
|
181
190
|
});
|
|
182
191
|
if (isUser) stampModeIfFirstInput(memory, settings, String(sessionId));
|
|
183
192
|
})),
|
|
@@ -352,6 +361,8 @@ export function captureSessionDiff(memory, session, settings = null) {
|
|
|
352
361
|
ts: time && !Number.isNaN(time) ? new Date(time).toISOString() : null,
|
|
353
362
|
model: null,
|
|
354
363
|
text,
|
|
364
|
+
// v14:与 session/event 那条路同一来源口径(同一条链的两个入口,口径不许分叉)
|
|
365
|
+
source: RAW_SOURCE_DSH,
|
|
355
366
|
});
|
|
356
367
|
added += 1;
|
|
357
368
|
if (type.startsWith('user')) userAdded += 1;
|