@nightpainters/dsh-ling 1.6.2 → 1.7.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/CHANGELOG.md +130 -4
- package/DESIGN.md +89 -6
- package/PHILOSOPHY.md +14 -4
- package/README.md +102 -18
- package/cordis.patch.yml +32 -2
- package/lib/client.js +2520 -55
- package/lib/host/agent-tools.js +286 -0
- package/lib/host/api.js +592 -59
- package/lib/host/archive.js +3 -3
- package/lib/host/autotree.js +2 -2
- package/lib/host/clock-skill.js +1 -1
- package/lib/host/config.js +38 -0
- package/lib/host/deepsummary.js +75 -2
- package/lib/host/dsweb-summary.js +6 -3
- package/lib/host/guard.js +53 -0
- package/lib/host/inject.js +12 -2
- package/lib/host/l1.js +13 -8
- package/lib/host/library-checker.js +552 -0
- package/lib/host/library-import-engine.js +211 -0
- package/lib/host/library-import.js +911 -0
- package/lib/host/library-runtime.js +199 -0
- package/lib/host/library-sqlite.js +344 -0
- package/lib/host/library-store.js +1714 -0
- package/lib/host/memory.js +73 -19
- package/lib/host/persona.js +13 -2
- package/lib/host/rawlog.js +1 -1
- package/lib/host/recall-search.js +45 -8
- package/lib/host/snapshot.js +2 -1
- package/lib/host/surgery.js +6 -4
- package/lib/host/tools.js +460 -19
- package/lib/host/util.js +1 -1
- package/lib/index.js +120 -81
- package/package.json +10 -5
- package/tools/deep-import-docs.mjs +1210 -0
package/lib/host/archive.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// dsh-ling host — 改动前的归档落盘(Part G · G2,2026-09-25)
|
|
2
2
|
//
|
|
3
|
-
// **原则(
|
|
3
|
+
// **原则(用户定):永远不做彻底删除。**
|
|
4
4
|
// 任何"清除/覆盖"类的操作,动手之前先把原数据**完整写到磁盘上的历史目录**,并附一份
|
|
5
5
|
// 人可读的改动日志 —— 谁、什么时候、为什么、以及怎么还原。
|
|
6
6
|
//
|
|
@@ -106,7 +106,7 @@ export function archiveBefore(memory, { source, convId, kind = 'forget', reason
|
|
|
106
106
|
/** 改动日志 —— 这份文件是给**将来的人**看的,所以写全:是什么、为什么、怎么还原。 */
|
|
107
107
|
function readme({ at, s, c, kind, reason, actor, turns }) {
|
|
108
108
|
const who = actor === 'ling' ? '鱼姬(工具通道)' : '主人(界面通道)';
|
|
109
|
-
// A-01(1.5.1):第三种 kind —— 主脉记忆覆盖(
|
|
109
|
+
// A-01(1.5.1):第三种 kind —— 主脉记忆覆盖(同一主脉再次采纳提炼结果;用户裁决"备份并覆盖")。
|
|
110
110
|
const what = kind === 'forget' ? '遗忘(软标记 —— **库里的行没有删**)'
|
|
111
111
|
: kind === 'vein-overwrite' ? '主脉记忆**覆盖**(同一主脉再次采纳 —— **上一版正文完整存在这里**)'
|
|
112
112
|
: '回灌(用归档恢复一条记忆 —— **覆盖前的状态已存这里**)';
|
|
@@ -150,7 +150,7 @@ ${restoreBlock}
|
|
|
150
150
|
|
|
151
151
|
## 为什么保留
|
|
152
152
|
|
|
153
|
-
|
|
153
|
+
忘错了要能回头。这是用户定的规矩:**永远不做彻底删除** ——
|
|
154
154
|
所以这个目录是"曾经发生过什么"的证据,不是垃圾。不要随手清。
|
|
155
155
|
|
|
156
156
|
> 生成于 dsh-ling · Part G(器灵侧正规通道)
|
package/lib/host/autotree.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// dsh-ling host — 一键生成记忆树(D9-b,2026-09-20
|
|
1
|
+
// dsh-ling host — 一键生成记忆树(D9-b,2026-09-20 与用户定案)。
|
|
2
2
|
//
|
|
3
3
|
// 主干 1593 条太粗,需要先把树"初步划一遍"。分两阶段:
|
|
4
4
|
// ① 机械分段 —— 按时序相邻性把全库概述切成若干簇(零成本、确定、可复现);
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// ③ 主人审核后落库(apply 在 api.js 侧) —— 建枝/主脉 + 挂会话。
|
|
7
7
|
//
|
|
8
8
|
// 为什么不把 1593 条直接丢给模型:装不进上下文,而且"按时序切段"本就是机械活。
|
|
9
|
-
// 模型擅长的是"给一段对话起主题名",不是"在 1593 条里做聚类"(设计稿 §8 /
|
|
9
|
+
// 模型擅长的是"给一段对话起主题名",不是"在 1593 条里做聚类"(设计稿 §8 / 用户 2026-09-20)。
|
|
10
10
|
//
|
|
11
11
|
// 不变量:本模块**只读概述索引**、只产出候选,**从不改写任何记忆内容**。
|
|
12
12
|
|
package/lib/host/clock-skill.js
CHANGED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import Schema from '@deepseek-ai/schemastery';
|
|
2
|
+
import { DEFAULT_LIBRARY_LIMITS } from './library-store.js';
|
|
3
|
+
import { normalizeLibraryConfig } from './library-runtime.js';
|
|
4
|
+
|
|
5
|
+
const libraryFields = Schema.object({
|
|
6
|
+
enabled: Schema.boolean().default(true).description('启用独立图书馆;不迁移记忆、不参与开场注入。'),
|
|
7
|
+
codeRoots: Schema.array(Schema.string()).default([]).description('管理员登记的绝对代码根目录。'),
|
|
8
|
+
officialUrls: Schema.array(Schema.string()).default([]).description('管理员登记的精确 HTTPS 文档 URL。'),
|
|
9
|
+
limits: Schema.object(Object.fromEntries(Object.entries(DEFAULT_LIBRARY_LIMITS)
|
|
10
|
+
.map(([key, value]) => [key, Schema.number().default(value)]))),
|
|
11
|
+
// F10(1.7 修复批次):图书馆配置校验失败的原因。非空 ⇒ **图书馆整块**不可用,记忆/人格/工具照常。
|
|
12
|
+
// 它是"响亮"的载体:理由词给上层,出错的键名给用户。
|
|
13
|
+
configError: Schema.string().default('').description('图书馆配置校验失败的原因(非空 ⇒ 图书馆整块不可用,其余功能照常)'),
|
|
14
|
+
});
|
|
15
|
+
const fields = Schema.object({ library: libraryFields });
|
|
16
|
+
|
|
17
|
+
// Validate raw keys before Schemastery fills defaults or adapts values. Its
|
|
18
|
+
// object resolver otherwise preserves unknown keys, including library hooks.
|
|
19
|
+
// Existing non-library settings keep their established configuration semantics.
|
|
20
|
+
export const Config = Schema.transform(Schema.any().default({}), (raw) => {
|
|
21
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
22
|
+
throw new TypeError('dsh-ling 配置必须是对象');
|
|
23
|
+
}
|
|
24
|
+
// F10(1.7 修复批次):以前 `normalizeLibraryConfig` 抛在这里 ⇒ 异常一路冒到 `apply()` ⇒
|
|
25
|
+
// **记忆 / 人格 / 全部 11 件工具一起不加载**,而且没有任何可指认面;而 `lib/index.js` 对
|
|
26
|
+
// "库打不开"却是有界重试 + 降级 + 排查指引 ⇒ 两种口径自相矛盾。
|
|
27
|
+
// 修法:**不吞、不静默** —— 原因带进 config(含出错的键名,可执行),图书馆进不可用态,
|
|
28
|
+
// 插件其余部分照常加载。**绝不**拿半套配置去开库(那会把"配置写错"变成看不懂的运行期怪象)。
|
|
29
|
+
let library = null;
|
|
30
|
+
let libraryConfigError = '';
|
|
31
|
+
try {
|
|
32
|
+
library = normalizeLibraryConfig(raw.library);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
libraryConfigError = String(error?.message ?? error).slice(0, 300);
|
|
35
|
+
library = normalizeLibraryConfig({}); // 结构仍是合法形状(默认:enabled=true、空白名单)
|
|
36
|
+
}
|
|
37
|
+
return fields({ ...raw, library: { ...library, configError: libraryConfigError } });
|
|
38
|
+
}, true).default({});
|
package/lib/host/deepsummary.js
CHANGED
|
@@ -8,6 +8,10 @@ import { RAW_IMPORT_PREFIX, rawSessionCandidates } from './memory.js';
|
|
|
8
8
|
import { pickSessionFile } from './backfill.js';
|
|
9
9
|
import { currentModelSelection } from './mode.js';
|
|
10
10
|
import { decodeAll } from './zstd.js';
|
|
11
|
+
// 「后台自动行为」的默认值真源(persona.DEFAULT_SETTINGS.auto)与区间真源(guard.AUTO_NUMBER_RANGES)
|
|
12
|
+
// 都各有唯一一处 —— 本模块只**取来用**,不另抄一份数字(抄一份 = 下次改默认必然漏改一处)。
|
|
13
|
+
import { DEFAULT_SETTINGS } from './persona.js';
|
|
14
|
+
import { AUTO_NUMBER_RANGES } from './guard.js';
|
|
11
15
|
|
|
12
16
|
// 循环外调用(深摘要 / 诞生仪式 / 语气归纳 / 自述起草)一律**跟随用户当前的模型选择**:
|
|
13
17
|
// 平台 API 要求显式给出 {provider, model},故读 agentDefaultModel.currentSelection();
|
|
@@ -23,10 +27,79 @@ export const DEEP_CFG = {
|
|
|
23
27
|
temperature: 0.2,
|
|
24
28
|
inputCap: 26000,
|
|
25
29
|
diskMinBytes: 64 * 1024,
|
|
26
|
-
autoIntervalMin: 30,
|
|
27
|
-
perRunLimit: 2,
|
|
30
|
+
autoIntervalMin: 30, // 旧默认值;1.7 起"自动间隔"的真源是 DEFAULT_SETTINGS.auto.deepIntervalMin(同值,见 resolveAuto)
|
|
31
|
+
perRunLimit: 2, // 仍是 deepCandidates 自己的默认每轮上限;启动块已改走 resolveAuto
|
|
28
32
|
};
|
|
29
33
|
|
|
34
|
+
/**
|
|
35
|
+
* 读出**用户真的写过**的 auto 子树(settings.json 只持久化用户补丁,见 settings-file.js 文件头)。
|
|
36
|
+
*
|
|
37
|
+
* 为什么不能用 `settings.get().auto` 判优先级:`get()` 把 DEFAULT_SETTINGS 深合并了进来
|
|
38
|
+
* ⇒ 那五个键**永远存在**,于是"代码默认值"会冒充"用户设定":插件配置里的旧键
|
|
39
|
+
* (`feedbackAuto` / `deepSummaryAuto` / `deepIntervalMin` / `deepMaxPerRun`)被永久压掉
|
|
40
|
+
* (老用户升级上来行为就变了),面板的 `source` 也会永远显示 settings。
|
|
41
|
+
* 入参两种都收:SettingsFile 实例(读 `.user.auto` —— 真实运行时)或普通设置对象(退回 `.auto`,测试桩用);
|
|
42
|
+
* **「有 .user 但没写 auto」与「没有 .user」必须分开**:前者 = 用户什么都没写 ⇒ `{}`,后者才回退到 `.auto`。
|
|
43
|
+
*/
|
|
44
|
+
export function storedAuto(settingsLike) {
|
|
45
|
+
const s = (settingsLike && typeof settingsLike === 'object') ? settingsLike : {};
|
|
46
|
+
const hasUser = (s.user && typeof s.user === 'object' && !Array.isArray(s.user)) ? s.user : null;
|
|
47
|
+
const cand = hasUser ? hasUser.auto : s.auto;
|
|
48
|
+
return (cand && typeof cand === 'object' && !Array.isArray(cand)) ? cand : {};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 「后台自动行为」的**唯一解析处**(2026-10-16 与用户定):settings.auto > 插件配置旧键 > 代码默认。
|
|
53
|
+
*
|
|
54
|
+
* 为什么要收成一支:三级优先级原先散在 `index.js` 的启动块里(且探活延时是裸的 12000、根本没有配置键),
|
|
55
|
+
* 而设置面板必须能如实显示"这个值是谁定的" —— 两处各判一次,迟早会漂成两个答案。
|
|
56
|
+
*
|
|
57
|
+
* 默认值**只从 `DEFAULT_SETTINGS.auto` 取**(不在这里另抄一份数字);区间只从 `AUTO_NUMBER_RANGES` 取。
|
|
58
|
+
* cfg 一侧刻意保留旧代码的 `Number(...)` 口径:插件配置里写 `'60'` 这种字符串,旧实现是生效的,
|
|
59
|
+
* 这里不能因为"更严格"就把它变成没设过(那才是行为变了)。
|
|
60
|
+
*
|
|
61
|
+
* @param {object} settings SettingsFile 实例(读用户补丁)或普通设置对象;见 storedAuto
|
|
62
|
+
* @param {object} [cfg] 插件配置(旧键名:`deepSummaryAuto` / `deepIntervalMin` / `deepMaxPerRun` / `feedbackAuto`)
|
|
63
|
+
* @returns {{deepSummary: boolean, deepIntervalMin: number, deepMaxPerRun: number,
|
|
64
|
+
* bootProbeDelayMs: number, feedbackAuto: boolean,
|
|
65
|
+
* source: Record<string, 'settings'|'config'|'default'>}}
|
|
66
|
+
*/
|
|
67
|
+
export function resolveAuto(settings, cfg = {}) {
|
|
68
|
+
const sAuto = storedAuto(settings);
|
|
69
|
+
const c = (cfg && typeof cfg === 'object' && !Array.isArray(cfg)) ? cfg : {};
|
|
70
|
+
const D = DEFAULT_SETTINGS.auto;
|
|
71
|
+
const source = {};
|
|
72
|
+
/** 布尔:settings 里必须是**真布尔**才算命中(字符串 "false" 不算设过 —— 与写面同一把尺)。 */
|
|
73
|
+
const boolOf = (key, cfgKey, def) => {
|
|
74
|
+
if (typeof sAuto[key] === 'boolean') { source[key] = 'settings'; return sAuto[key]; }
|
|
75
|
+
if (cfgKey && typeof c[cfgKey] === 'boolean') { source[key] = 'config'; return c[cfgKey]; }
|
|
76
|
+
source[key] = 'default'; return def;
|
|
77
|
+
};
|
|
78
|
+
/** 数字:settings 收有限数;cfg 沿用 `?? + Number()` 的旧口径(只有 undefined/null 算"没写");
|
|
79
|
+
* 非有限数(如 'abc')落到默认 —— 旧实现会把它变成 NaN(interval 变 0 = 空转),那不是要保留的行为。 */
|
|
80
|
+
const numOf = (key, cfgKey, def) => {
|
|
81
|
+
const [lo, hi] = AUTO_NUMBER_RANGES[key];
|
|
82
|
+
let raw;
|
|
83
|
+
if (typeof sAuto[key] === 'number' && Number.isFinite(sAuto[key])) { raw = sAuto[key]; source[key] = 'settings'; }
|
|
84
|
+
else {
|
|
85
|
+
const has = cfgKey !== undefined && cfgKey !== null && c[cfgKey] !== undefined && c[cfgKey] !== null;
|
|
86
|
+
const cv = has ? Number(c[cfgKey]) : NaN;
|
|
87
|
+
if (has && Number.isFinite(cv)) { raw = cv; source[key] = 'config'; }
|
|
88
|
+
else { raw = def; source[key] = 'default'; }
|
|
89
|
+
}
|
|
90
|
+
return Math.min(hi, Math.max(lo, raw));
|
|
91
|
+
};
|
|
92
|
+
return {
|
|
93
|
+
// 探活延时**没有**旧配置键(此前硬编码 12000)⇒ 它只可能来自 settings 或默认。
|
|
94
|
+
deepSummary: boolOf('deepSummary', 'deepSummaryAuto', D.deepSummary),
|
|
95
|
+
deepIntervalMin: numOf('deepIntervalMin', 'deepIntervalMin', D.deepIntervalMin),
|
|
96
|
+
deepMaxPerRun: numOf('deepMaxPerRun', 'deepMaxPerRun', D.deepMaxPerRun),
|
|
97
|
+
bootProbeDelayMs: numOf('bootProbeDelayMs', null, D.bootProbeDelayMs),
|
|
98
|
+
feedbackAuto: boolOf('feedbackAuto', 'feedbackAuto', D.feedbackAuto),
|
|
99
|
+
source,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
30
103
|
/** 解析循环外调用使用的模型:跟随当前选择;不可读则用兜底值并留一行 debug。 */
|
|
31
104
|
export function resolveLlmTarget(ctx) {
|
|
32
105
|
const cur = typeof currentModelSelection === 'function' ? currentModelSelection(ctx) : null;
|
|
@@ -137,7 +137,7 @@ export async function probeAssistant(baseUrl = ASSISTANT_DEFAULT, { timeoutMs =
|
|
|
137
137
|
* · **Ollama 原生 `/api/chat` + `think:false` 有效**:实测 thinking 长度 0、正文正常。
|
|
138
138
|
* 因此默认走原生端点(把 base 末尾的 `/v1` 去掉),`noThink:false` 时才用兼容端点。
|
|
139
139
|
*/
|
|
140
|
-
export async function summarizeLocal(baseUrl, model, { system = SUMMARY_SYS, text, maxTokens = MAX_TOKENS_DEFAULT, timeoutMs = 90000, noThink = true } = {}) {
|
|
140
|
+
export async function summarizeLocal(baseUrl, model, { system = SUMMARY_SYS, text, maxTokens = MAX_TOKENS_DEFAULT, timeoutMs = 90000, noThink = true, clean = true } = {}) {
|
|
141
141
|
const t0 = Date.now();
|
|
142
142
|
const base = String(baseUrl).replace(/\/+$/, '');
|
|
143
143
|
const native = base.replace(/\/v1$/, '');
|
|
@@ -181,8 +181,11 @@ export async function summarizeLocal(baseUrl, model, { system = SUMMARY_SYS, tex
|
|
|
181
181
|
completionTokens: noThink ? (j?.eval_count ?? null) : (j?.usage?.completion_tokens ?? null),
|
|
182
182
|
promptTokens: noThink ? (j?.prompt_eval_count ?? null) : (j?.usage?.prompt_tokens ?? null),
|
|
183
183
|
};
|
|
184
|
-
|
|
185
|
-
|
|
184
|
+
// `clean:false`(2026-10-12,给图书馆导入通道的模型段用):**原样交出模型回复**。
|
|
185
|
+
// 为什么必须有这一档:`cleanSummary` 会把回复压成一行、截到 300 字 —— 对"一句摘要"正好,
|
|
186
|
+
// 对**要 JSON 的调用方**等于把结构撕碎(它连换行都吃掉)。摘要路径一个字节没变(默认仍是 clean:true)。
|
|
187
|
+
const summary = clean ? cleanSummary(raw) : String(raw || '').trim();
|
|
188
|
+
return { ok: clean ? !!summary : summary !== '', summary, raw, error: (clean ? !!summary : summary !== '') ? null : (diag.reasoningLen > 0 ? 'reasoning-only' : 'empty'), ms: Date.now() - t0, diag };
|
|
186
189
|
} catch (e) {
|
|
187
190
|
return { ok: false, error: String(e?.name === 'AbortError' ? 'timeout' : (e?.message ?? e)).slice(0, 120), ms: Date.now() - t0, path: noThink ? 'native' : 'compat' };
|
|
188
191
|
} finally {
|
package/lib/host/guard.js
CHANGED
|
@@ -219,3 +219,56 @@ export function sanitizePersonaPatch(patch) {
|
|
|
219
219
|
}
|
|
220
220
|
return { clean, dropped };
|
|
221
221
|
}
|
|
222
|
+
|
|
223
|
+
// ── ⑤ 后台行为补丁白名单(2026-10-16)──────────────────────────────────────────────
|
|
224
|
+
// 与 ④ 同一纪律、同一原因:`POST /auto` 也把 body 交给 `settings.update`(深合并,**不做键过滤**)
|
|
225
|
+
// ⇒ 不设白名单就等于给了一条新旁路 —— 拿 `/auto` 往 settings.json 里写 `guard.enforce:false` 或
|
|
226
|
+
// 整棵 persona(④ 那次堵的正是这条链)。故:只有下面 5 个键能出这个函数。
|
|
227
|
+
// 额外收窄(比 ④ 更严):**只收扁平对象**。auto 子树没有嵌套语义,接受嵌套/数组纯粹是给原型污染留门
|
|
228
|
+
// (`{"__proto__":{…}}` 经深合并会落到 Object.prototype 上,那是全进程级污染,不是一个设置项)。
|
|
229
|
+
/** auto 子树里允许经 /auto 写入的键(= DEFAULT_SETTINGS.auto 的全部字段)。 */
|
|
230
|
+
export const AUTO_PATCH_KEYS = Object.freeze([
|
|
231
|
+
'deepSummary', 'deepIntervalMin', 'deepMaxPerRun', 'bootProbeDelayMs', 'feedbackAuto',
|
|
232
|
+
]);
|
|
233
|
+
/** 布尔键(其余三个是数字键:类型不符一律丢弃,不猜用户想说什么)。 */
|
|
234
|
+
export const AUTO_BOOL_KEYS = Object.freeze(['deepSummary', 'feedbackAuto']);
|
|
235
|
+
/** 数字键的合法区间 [lo, hi]:**唯一定义处** —— sanitizeAutoPatch(写面)与 resolveAuto(读面)同取这一份,
|
|
236
|
+
* 两处各写一遍迟早会漂成"写得进 5、读出来 10"这种自相矛盾的设置。 */
|
|
237
|
+
export const AUTO_NUMBER_RANGES = Object.freeze({
|
|
238
|
+
deepIntervalMin: Object.freeze([10, 1440]),
|
|
239
|
+
deepMaxPerRun: Object.freeze([1, 20]),
|
|
240
|
+
bootProbeDelayMs: Object.freeze([0, 600000]),
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* 把一份「后台行为」补丁收窄成"只能落在这 5 个白名单键上"的安全扁平补丁。
|
|
245
|
+
* @param {unknown} patch 待写入的补丁(通常是 body.patch,也接受 body 本身)
|
|
246
|
+
* @returns {{ clean: object, dropped: string[], clamped: string[] }} clean = 可交给 settings.update 的补丁;
|
|
247
|
+
* dropped = 被丢弃的键(白名单外 / 类型不符 / 整个 body 不是普通对象);
|
|
248
|
+
* clamped = 被夹取到区间的键(用于如实回报"你填的值没被原样采纳")
|
|
249
|
+
*/
|
|
250
|
+
export function sanitizeAutoPatch(patch) {
|
|
251
|
+
const clean = {};
|
|
252
|
+
const dropped = [];
|
|
253
|
+
const clamped = [];
|
|
254
|
+
// 数组与 null 都不是补丁(数组的 Object.entries 会给出 "0"/"1" 这类下标键,没有语义)
|
|
255
|
+
if (!patch || typeof patch !== 'object' || Array.isArray(patch)) return { clean, dropped, clamped };
|
|
256
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
257
|
+
if (!Object.prototype.hasOwnProperty.call(AUTO_NUMBER_RANGES, k)) {
|
|
258
|
+
// 布尔键:只收真正的布尔;字符串 "false" 不猜(猜错方向 = 悄悄关掉一个后台行为)
|
|
259
|
+
if (AUTO_BOOL_KEYS.includes(k)) {
|
|
260
|
+
if (typeof v === 'boolean') clean[k] = v;
|
|
261
|
+
else dropped.push(k);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
dropped.push(k);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (typeof v !== 'number' || !Number.isFinite(v)) { dropped.push(k); continue; }
|
|
268
|
+
const [lo, hi] = AUTO_NUMBER_RANGES[k];
|
|
269
|
+
const c = Math.min(hi, Math.max(lo, v));
|
|
270
|
+
clean[k] = c;
|
|
271
|
+
if (c !== v) clamped.push(k);
|
|
272
|
+
}
|
|
273
|
+
return { clean, dropped, clamped };
|
|
274
|
+
}
|
package/lib/host/inject.js
CHANGED
|
@@ -88,7 +88,7 @@ function advanceLogWatermark(memory, sessionId, maxId, shownMaxId, retiredMaxId,
|
|
|
88
88
|
if (retired > 0 && retired > Number(memory.kvGet('branch_log_wm.retired') || 0)) {
|
|
89
89
|
memory.kvSet('branch_log_wm.retired', String(retired));
|
|
90
90
|
}
|
|
91
|
-
// 复盘守门(2026-09-22
|
|
91
|
+
// 复盘守门(2026-09-22 用户:「动记忆树不能是随意的,应该是慎重严肃的」):
|
|
92
92
|
// 另记一个**全局最后同步**水位,回答"自器灵上次开口以来累积了多少笔改动还没交代"
|
|
93
93
|
// (memory.branchLogPending 读它)。与会话水位互补 —— 会话水位管"这个会话看到什么",
|
|
94
94
|
// 本键管"是否还没跟器灵交代过"。
|
|
@@ -96,7 +96,17 @@ function advanceLogWatermark(memory, sessionId, maxId, shownMaxId, retiredMaxId,
|
|
|
96
96
|
} catch { /* 推进失败仅导致下次重复提示,不影响注入 */ }
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
/** 注册记忆注入段(order 1500)+ 时间锚 context。
|
|
100
|
+
*
|
|
101
|
+
* ── F5(1.7 修复批次,2026-10-15)────────────────────────────────────────────
|
|
102
|
+
* 第 5 参是**逃生开关**:`cordis.patch.yml:16` 早就承诺 `injectEnabled: false` 能关掉注入,
|
|
103
|
+
* 但在这次修之前全仓 `cfg.injectEnabled` **零处读** ⇒ 用户以为关掉了、实际照注(静默失效)。
|
|
104
|
+
* 现在关到底 ⇒ **一段都不注册**(section 与 clock context 都不挂),留痕仍由
|
|
105
|
+
* index.js 的 `inject_enabled_config` 负责。默认(true / 不传)行为**逐字不变**。
|
|
106
|
+
* ⚠️ 这一道只管**记忆段**;人格段走 `settings.persona.enabled`(persona.js:166),两者并列。
|
|
107
|
+
* 回归:tests/injectlog.test.mjs 第 5 节(关到底零注册 + 默认行为正对照 + 接线判据)。 */
|
|
108
|
+
export function registerMemorySection(ctx, gate, memory, settings, { enabled = true } = {}) {
|
|
109
|
+
if (enabled === false) return () => {};
|
|
100
110
|
const sp = svc(ctx, 'systemPrompt');
|
|
101
111
|
if (!sp || typeof sp.section !== 'function') {
|
|
102
112
|
console.debug('[dsh-ling] systemPrompt service unavailable; injection disabled');
|
package/lib/host/l1.js
CHANGED
|
@@ -95,7 +95,7 @@ function kwOverlap(queryKeywords, rowKeywords) {
|
|
|
95
95
|
* (为"查询 4-gram vs 存储 6-gram 的**关键词表**"设计的),而条目路的右操作数是**条目正文**
|
|
96
96
|
* (全库均 52 字)。中文常用 3-gram 遍地都是 ⇒ 几乎任何查询都会命中 3 条以上 ⇒ 打满
|
|
97
97
|
* `KW_BONUS_CAP`(+1.8),把新近(最多 0.3)和 hit(尚为 0)全压死。
|
|
98
|
-
* 实测病灶(
|
|
98
|
+
* 实测病灶(用户 2026-10-04 开新会话看到):开场 8 条**全是同一次生图会话的条目** ——
|
|
99
99
|
* 排序实际上等于"谁的字面碰巧撞上查询词"。
|
|
100
100
|
* 长句只做**连续子串**匹配:那个词真出现在那句话里才算命中。 */
|
|
101
101
|
function kwHitInText(queryKeywords, text) {
|
|
@@ -154,7 +154,7 @@ const HIT_SATURATION = 5; // 采用满 5 次即拿满分(与记忆行 freq
|
|
|
154
154
|
// 新近对**结论层**本就该是弱信号(结论的价值在长期),压到 0.1 后不再主导。
|
|
155
155
|
const RECENCY_W = 0.1;
|
|
156
156
|
const DEEP_HALF_LIFE = 365; // 半衰期一年(旧的概述行是 90 天)
|
|
157
|
-
/** **硬度分**(1.6,2026-10-04
|
|
157
|
+
/** **硬度分**(1.6,2026-10-04 用户的判断:「能不能做成从根源上判断这些条目到底是临时的还是永久的,
|
|
158
158
|
* 或者加"硬度"评分来衡量临时程度/永久程度」)。
|
|
159
159
|
* 它**同时**用于两处:出局(见 `selectL1` 里 `ephemeral` 的 continue)与**排序**。
|
|
160
160
|
* 只做前者是不够的 —— 上一版就只做了前者,于是"长期原则"和"阶段性结论"在排序里**同权**,
|
|
@@ -305,7 +305,7 @@ export function selectL1(store, opts) {
|
|
|
305
305
|
const now = Date.now();
|
|
306
306
|
const summaryBonus = Number.isFinite(Number(o.summaryBonus)) ? Number(o.summaryBonus) : SUMMARY_BONUS_DEFAULT;
|
|
307
307
|
// 2026-09-21:归档会话不参与召回 —— 归档是主人的明确动作("这条别再提了"),
|
|
308
|
-
// 而此前 selectL1 直接吃 listOverviews 全量:归档只影响界面,召回照旧(
|
|
308
|
+
// 而此前 selectL1 直接吃 listOverviews 全量:归档只影响界面,召回照旧(用户实测反馈)。
|
|
309
309
|
// 只对 source='dsh' 生效:archived 是会话级标记,历史网页端/导入条目没有会话行。
|
|
310
310
|
const archived = (() => {
|
|
311
311
|
try {
|
|
@@ -353,7 +353,7 @@ export function selectL1(store, opts) {
|
|
|
353
353
|
// 真库实测 **393/393 条全为空**(schema 注释早写明"记忆树是空间维度、深层库是内容维度,
|
|
354
354
|
// 条目不隶属任何枝"),照那个字段接等于"接了也没接"。走会话归属才有意义:
|
|
355
355
|
// dsh 侧实测主干 73 / 开发日志 25 / 运维排障 18 / 日常 4,非 dsh 侧已挂枝 34、未挂枝 179。
|
|
356
|
-
// 取值:默认 1.0 ⇒ **接线当下零行为变化**(
|
|
356
|
+
// 取值:默认 1.0 ⇒ **接线当下零行为变化**(枝系数没被调过时,一切排序与接线前逐字相同);
|
|
357
357
|
// 复盘期调过谁,谁的记忆才在开场里前后移动 —— 这就是 `README` U-10「装饰性交互要么真生效、
|
|
358
358
|
// 要么删掉」要的那个"真生效"。
|
|
359
359
|
// ⚠️ 它乘在**最后**一项(`raw × 血缘 × 矛盾 × 枝系数`),与 `LINEAGE_*` 同一层:*打对折*,
|
|
@@ -402,7 +402,12 @@ export function selectL1(store, opts) {
|
|
|
402
402
|
// 而 `MAX_PER_CONV` 压的是症状(临时结论往往扎堆在一次会话里)⇒ 两者并存、分工不同。
|
|
403
403
|
if (String(row.durability || 'long') === 'ephemeral') { excluded.ephemeral += 1; continue; }
|
|
404
404
|
const hit = Number(row.hit_count ?? 0);
|
|
405
|
-
|
|
405
|
+
// ⚠️ 2026-10-06:**类目名(kind)也参与关键词命中** —— 修前只匹配 `row.text`,于是查询里的
|
|
406
|
+
// 类目词打不中它们**自己的**条目:实测搜「纪律」时那 18 条纪律一条都加不上分(`kw` 恒为 0),
|
|
407
|
+
// 开场里只靠权重挤进 2 席 —— 同族缺陷对"承诺 / 决定 / 关系"一样成立。
|
|
408
|
+
// kind 是 2 字精确词、且 `kwHitInText` 只做**连续子串**匹配(不共享 n-gram)
|
|
409
|
+
// ⇒ 不会重蹈它上面那段注释里"3-gram 遍地都是、几乎任何查询都打满"的覆辙。
|
|
410
|
+
const kw = kwHitInText(o.keywords || [], row.text + ' ' + String(row.kind || ''));
|
|
406
411
|
const t = toEpochMs(row.at || row.created_at);
|
|
407
412
|
const recency = decayWeight(t === null ? 0 : Math.max(0, (now - t) / DAY_MS), DEEP_HALF_LIFE);
|
|
408
413
|
// 硬度参与排序(不只是出局):`durable` 抬 0.6,与 `long` 拉开约 7 倍 ⇒ 长期原则/承诺/关系
|
|
@@ -439,7 +444,7 @@ export function selectL1(store, opts) {
|
|
|
439
444
|
.filter((x) => !x.pinned)
|
|
440
445
|
.sort((a, b) => b.score - a.score
|
|
441
446
|
|| String(b.row.at || b.row.created_at || '').localeCompare(String(a.row.at || a.row.created_at || '')));
|
|
442
|
-
// **同源限额**(2026-10-04
|
|
447
|
+
// **同源限额**(2026-10-04 用户实测:「会话内的、临时的结论会被灌入」):
|
|
443
448
|
// 同一次会话最多进 `MAX_PER_CONV` 条 —— 否则一次"高产出"的会话能把整个开场占满
|
|
444
449
|
// (实测某次生图会话有 13 条,开场 8 条里它一家全占)。同源被挡下的**进落选者名单**
|
|
445
450
|
// (`reason='conv'`),这样"它为什么没进"仍可查,而不是凭空消失。
|
|
@@ -471,7 +476,7 @@ export function selectL1(store, opts) {
|
|
|
471
476
|
const keepPinned = ranked.filter((x) => x.pinned);
|
|
472
477
|
const restRanked = ranked.filter((x) => !x.pinned);
|
|
473
478
|
const restQuota = Math.max(0, max - keepPinned.length);
|
|
474
|
-
// **事实席位**(
|
|
479
|
+
// **事实席位**(用户 2026-10-04:「8 条也可以考虑改 10 条,其中 2 条作为最硬的事实的席位」)——
|
|
475
480
|
// 动机是他看到的偏斜:承诺/关系优先之后,「我在什么环境里干活」这类**稳定事实**因为类别分为 0
|
|
476
481
|
// 而永远进不来;但它们确实是"接下来要干什么"的前提。⇒ 10 个位置里留 2 个给**最硬的事实**
|
|
477
482
|
// (`durable` 的「事实」类),既不挤占"关于人"的主体(那 8 个),也不让环境事实缺席。
|
|
@@ -634,7 +639,7 @@ const L1_FRAME_CALLNAME_FALLBACK = '主人';
|
|
|
634
639
|
/**
|
|
635
640
|
* 视角框的称呼 —— **跟随 `persona.userTitle`**,不硬编码。
|
|
636
641
|
*
|
|
637
|
-
* 为什么走配置而不是写死(2026-10-04
|
|
642
|
+
* 为什么走配置而不是写死(2026-10-04 用户的判断:「提早换更合适」):
|
|
638
643
|
* - `dsh-ling` 是要发 npm 的通用包,**把某个用户的名字写死会污染公开件**;
|
|
639
644
|
* - `userTitle` 本来就是**主人自己可编辑**的字段(`persona.js`:「空 = 用"你"」),
|
|
640
645
|
* 走它 = 本机显示他设的称呼、别人的实例显示他们自己的,**本机效果不打折**;
|