@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/memory.js
CHANGED
|
@@ -9,13 +9,18 @@ import { ensureDir, sha256Text, utcIso, mergeDeep, toIso, toEpochMs } from './ut
|
|
|
9
9
|
// 且把 memory 当参数传(依赖注入)⇒ memory.js 引它**不成环**。
|
|
10
10
|
import { archiveBefore } from './archive.js';
|
|
11
11
|
|
|
12
|
-
const SCHEMA_VERSION = 17;
|
|
12
|
+
const SCHEMA_VERSION = 17;
|
|
13
|
+
/** 作废哨兵(`deep_item.superseded_by` 的第三个取值,2026-10-15)。
|
|
14
|
+
* 为什么用哨兵而不是新列:所有"排除已被取代"的读路径(开场选择 / recall / 面板)
|
|
15
|
+
* 判的都是 `superseded_by != ''` —— 哨兵让**那些路径一处都不用改**就自动不再挑它;
|
|
16
|
+
* 而"为什么作废"落在 kv `deep.retract:<id>`(人可读、可审计),不污染表语义。 */
|
|
17
|
+
export const DEEP_RETRACTED = 'retracted'; // v17:deep_item.durability(条目「硬度」,1.6,2026-10-04 —— 主人原话「同会话使用硬数量上限来卡吗,这可能有点机械,能不能做成从根源上判断这些条目到底是临时的还是永久的」⇒ 病根不是"同一会话进了太多条",而是**临时的东西被判成了长期记忆**);v16:deep_item(深层库条目,1.6 步 2,2026-10-04 —— 纯新增表,DDL 的 IF NOT EXISTS 即迁移;与记忆树并立、条目不被枝隶属);v15:inject_log(投递留痕,1.6-A,2026-10-03 —— 纯新增表,DDL 的 IF NOT EXISTS 即迁移);v14:dsh_turns_raw.source(来源从"推断"变"记录",2026-10-01 主人定「来源也加个列吧」—— 旧判据是"裸 id = DSH 会话",边界是"将来若出现不带冒号的新来源名,物理上分不出来");v13:forgotten(软遗忘标记 —— 永远不彻底删除,先归档再打标记);v12:branch_log.actor(G4 器灵侧通道 —— 工具改的树必须记在"鱼姬"名下,不再一律记成主人);v11:vein_suggestions(主脉提炼审核队列 R7 —— 设计稿 §5:提炼必须走审核,不直接进记忆);
|
|
13
18
|
// v10:tree_snapshot.kind('manual'|'auto',审计 A#9 —— 自动备份与手动快照此前外表完全一致,删错了就没有撤回路径);v9:tree_snapshot(树的保存/恢复 R1);v8:conv_branch(非会话源的枝归属覆盖层,方案 A);v7:branch_log(复盘留痕);v6:vein_link + memory_conflict(D9-b 树的生长);v5:branch(记忆分枝 D9-a);v4:title_by(定名者:user/ai);v3:title_locked;v2:conv_overview.source 放开 CHECK
|
|
14
19
|
|
|
15
20
|
/** 主干枝 id(D9-a 记忆分枝,2026-09-18):存量会话与记忆天然属于主干。 */
|
|
16
21
|
export const TRUNK_ID = 'trunk';
|
|
17
22
|
|
|
18
|
-
/** 血缘档位(
|
|
23
|
+
/** 血缘档位(用户 2026-09-18 定档 1.0/0.7/0.4)。
|
|
19
24
|
* 取"最弱环"单值,不做连乘 —— `0.7^5=0.168`、`0.4^5=0.010`,连乘会让深枝等于从记忆里消失,
|
|
20
25
|
* 且每层舍入都会乘进后面。单值 + 有界,权重才可解释、可复现。 */
|
|
21
26
|
export const LINEAGE_SAME = 1.0; // 同枝
|
|
@@ -327,6 +332,9 @@ CREATE TABLE IF NOT EXISTS deep_item (
|
|
|
327
332
|
-- ⚠️ ephemeral **不进开场**(选择侧过滤掉),但**照旧留在库里、recall 搜得到**:
|
|
328
333
|
-- 判严的代价远小于判松了(判松 = 每次开口都在推一条早失效的临时决定)。
|
|
329
334
|
superseded_by TEXT NOT NULL DEFAULT '', -- 被哪条取代 —— 沿用老规矩「只追不替」:旧的留痕、不删
|
|
335
|
+
-- 取值三态:'' = 在役 / 'di:…' = 被这条取代 /
|
|
336
|
+
-- 'retracted' = **作废**(无接替者,2026-10-15 用户拍板;
|
|
337
|
+
-- 理由落 kv「deep.retract:<id>」,见 DEEP_RETRACTED)
|
|
330
338
|
origin TEXT NOT NULL DEFAULT 'auto', -- auto(提炼) | user(主人手写);origin='user' 的不许被自动覆盖
|
|
331
339
|
branch_id TEXT NOT NULL DEFAULT '', -- 气根:空 = 未挂枝;非空 = 被这条枝反向引用(不参与树的层级计算)
|
|
332
340
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
@@ -402,7 +410,7 @@ function migrateTitleCols(db) {
|
|
|
402
410
|
}
|
|
403
411
|
}
|
|
404
412
|
|
|
405
|
-
/** v4→v5:记忆分枝(D9-a,2026-09-18
|
|
413
|
+
/** v4→v5:记忆分枝(D9-a,2026-09-18 用户定方向)。
|
|
406
414
|
* session_meta 加 branch_id(默认 'trunk' —— 存量会话天然属主干,迁移只填一列);
|
|
407
415
|
* 建 branch 表并保证主干行存在。**记忆行不冗余存 branch**,靠 session → branch 推导:
|
|
408
416
|
* 迁移成本最低,且枝归属变化时无需回写海量记忆行。 */
|
|
@@ -941,10 +949,12 @@ export class MemoryStore {
|
|
|
941
949
|
|
|
942
950
|
/** 复盘期权重调节(平时只读 —— 设计稿 §3.3)。scale 夹在 [0,2]。
|
|
943
951
|
*
|
|
944
|
-
*
|
|
945
|
-
|
|
946
|
-
*
|
|
947
|
-
*
|
|
952
|
+
* **A-5(2026-10-05 夜 · 主人拍板走方案 b):`weight_scale` 已接回检索打分** ——
|
|
953
|
+
* L1 = `raw × 血缘档位 × 矛盾系数 × 枝系数`。
|
|
954
|
+
* 历史:审计 A#11(2026-09-22)曾把**权重与可见性一起**定为"只存只显示";2026-10-05 只解开权重那一半
|
|
955
|
+
* (可见性仍是预留字段,见 `listBranches`)。
|
|
956
|
+
* ⚠️ 别再把权重这半边叫"预留字段"(2026-10-06 内部台账核对抓到:上面这行旧的"预留字段"与这行
|
|
957
|
+
* "已接回打分"**在同一个注释块里自相矛盾**,读第一行的人会判它无效)。 */
|
|
948
958
|
setBranchWeight(branchId, scale) {
|
|
949
959
|
const bid = String(branchId || '');
|
|
950
960
|
// ⚠️ 先判**类型**再取数(蓝队 2026-10-04):`Number(null) === 0`、`Number(true) === 1`、
|
|
@@ -959,7 +969,8 @@ export class MemoryStore {
|
|
|
959
969
|
return {
|
|
960
970
|
ok: true, id: bid, weightScale: clamped,
|
|
961
971
|
applied: true,
|
|
962
|
-
note: '已生效:该枝的结论在记忆开场里按此系数加权(1.0 =
|
|
972
|
+
note: '已生效:该枝的结论在记忆开场里按此系数加权(1.0 = 不偏不倚;⚠️ **0 = 该枝结论不进开场** —— '
|
|
973
|
+
+ '它会被当成"算不出正分"直接排除,所以 0 不是"只是排后面")',
|
|
963
974
|
};
|
|
964
975
|
}
|
|
965
976
|
|
|
@@ -1017,7 +1028,7 @@ export class MemoryStore {
|
|
|
1017
1028
|
|
|
1018
1029
|
// ---- 软遗忘(Part G G2,2026-09-25) ----
|
|
1019
1030
|
//
|
|
1020
|
-
// 原则(
|
|
1031
|
+
// 原则(用户 2026-09-25 定):**永远不做彻底删除**。遗忘 = 先把原数据归档落盘(原文 + 概述 +
|
|
1021
1032
|
// 改动日志),再在库里打一个标记,召回路径据标记排除。行仍在库里、归档文件在磁盘上 ——
|
|
1022
1033
|
// 两条独立还原路径,任何一条都能反悔。
|
|
1023
1034
|
//
|
|
@@ -1147,7 +1158,7 @@ export class MemoryStore {
|
|
|
1147
1158
|
*
|
|
1148
1159
|
* **调用方必须先归档当前状态** —— 本方法只管写,不做任何备份。
|
|
1149
1160
|
* 回灌的危险性高于遗忘:遗忘会留下"少了一条"的可见痕迹,而回灌是**往里加东西**,
|
|
1150
|
-
* 加错了很难察觉(
|
|
1161
|
+
* 加错了很难察觉(用户 2026-09-25:「backfill 比 forget 危险多了,因为它更难以觉察」)。
|
|
1151
1162
|
*
|
|
1152
1163
|
* 两道保护:① 写回的概述带 `origin` 标记(默认 `backfill-v1`),界面上看得出"这条是灌回来的";
|
|
1153
1164
|
* ② 不传 `keepUserState:false` ⇒ **不动主人留下的命中数与置顶**,回灌只补内容、不夺用户态。
|
|
@@ -1378,7 +1389,7 @@ export class MemoryStore {
|
|
|
1378
1389
|
// 换行能穿透引号、在系统提示里自造独立行(实测:note 塞 `\n\n【系统】…` 即可脱离框架)。
|
|
1379
1390
|
// 显示面修正(2026-09-22 实测):原实现对一切"查不到名字的 id"回退成「(已删枝 …)」,
|
|
1380
1391
|
// 但 branch_log 的 before/after **不只装枝 id** —— snapshot 装的是快照号(#9)、
|
|
1381
|
-
// assign 装的是记忆条目 id(026ebd86),二者都被误标成"已删枝"(
|
|
1392
|
+
// assign 装的是记忆条目 id(026ebd86),二者都被误标成"已删枝"(用户截图所见:
|
|
1382
1393
|
// `snapshot:主干 → (已删枝 #9)` 实际一根枝都没删)。先按 id 形态分流再回退查名。
|
|
1383
1394
|
const nm = (v) => {
|
|
1384
1395
|
const k = String(v || '');
|
|
@@ -1495,7 +1506,7 @@ export class MemoryStore {
|
|
|
1495
1506
|
}
|
|
1496
1507
|
}
|
|
1497
1508
|
|
|
1498
|
-
/** 尚未同步给器灵的改动笔数(复盘守门,2026-09-22
|
|
1509
|
+
/** 尚未同步给器灵的改动笔数(复盘守门,2026-09-22 用户:「动记忆树不能是随意的」)。
|
|
1499
1510
|
*
|
|
1500
1511
|
* 判据:自器灵**上一次开口**(注入推进水位)以来累积了多少笔结构改动。
|
|
1501
1512
|
* 水位取 `branch_log_wm.last`(由 `inject.js` 的 advanceLogWatermark 在投递时写入),
|
|
@@ -1516,7 +1527,7 @@ export class MemoryStore {
|
|
|
1516
1527
|
let pending = 0;
|
|
1517
1528
|
let maxId = 0;
|
|
1518
1529
|
try {
|
|
1519
|
-
// 'snapshot'(保存快照)不计入待交代笔数(2026-09-25
|
|
1530
|
+
// 'snapshot'(保存快照)不计入待交代笔数(2026-09-25 用户:「单纯保存不弹修改」)。
|
|
1520
1531
|
// 依据:保存快照**不改树**——它只是把当前结构另存一份,是"大改之前的保险",
|
|
1521
1532
|
// 不是"改动"。把它算成债会让主人每存一次就看到「⚠ 有 N 笔改动还没跟器灵说过」,
|
|
1522
1533
|
// 而实际上树一个字节没变。留痕仍写进 branch_log(审计价值保留),只是不推进债。
|
|
@@ -1587,7 +1598,7 @@ export class MemoryStore {
|
|
|
1587
1598
|
if (p && n.id !== TRUNK_ID) p.children.push(n);
|
|
1588
1599
|
else roots.push(n);
|
|
1589
1600
|
}
|
|
1590
|
-
// 层级 = **计算属性**(
|
|
1601
|
+
// 层级 = **计算属性**(用户 2026-09-19 定):按相对深度切,树长高时自动重算,**零回写**。
|
|
1591
1602
|
// 于是"几条主脉并成一条更大的主脉 ⇒ 原节点自动降级"是视图效果,不需要回写任何一行记忆 ——
|
|
1592
1603
|
// 这正是"任何树操作都不改写记忆内容"这条核心不变量能成立的原因。
|
|
1593
1604
|
// 档位:0=主干(恒 1 条);(0,0.25]=主脉;≤0.62=枝;其余=叶 ⇒ 最多 1 主干 / 2 主脉 / 3 枝 / 4 叶。
|
|
@@ -1625,7 +1636,7 @@ export class MemoryStore {
|
|
|
1625
1636
|
convBranch: this.db.prepare('SELECT source, conv_id, branch_id FROM conv_branch').all(),
|
|
1626
1637
|
links: this.db.prepare('SELECT from_branch, to_branch, kind, note FROM vein_link').all(),
|
|
1627
1638
|
};
|
|
1628
|
-
// 名字留空 → 自动命名;不写「未命名」(2026-09-22
|
|
1639
|
+
// 名字留空 → 自动命名;不写「未命名」(2026-09-22 用户:手工/自动 + 日期时间即可)
|
|
1629
1640
|
const nm = String(name || '').trim() || (k === 'auto' ? '自动快照 - ' : '手动快照 - ') + localStamp();
|
|
1630
1641
|
const r = this.db.prepare('INSERT INTO tree_snapshot (at,name,note,kind,data) VALUES (?,?,?,?,?)')
|
|
1631
1642
|
.run(now, nm.slice(0, 80), String(note || '').slice(0, 300), k, JSON.stringify(data));
|
|
@@ -2009,7 +2020,7 @@ export class MemoryStore {
|
|
|
2009
2020
|
}
|
|
2010
2021
|
|
|
2011
2022
|
/** 检索侧降权映射:`"source\\u0000convId" → 系数`。
|
|
2012
|
-
* 规则(
|
|
2023
|
+
* 规则(用户 2026-09-20 定):**复盘裁定的按裁定;未复盘的以最新为准**(旧的一方降权)。
|
|
2013
2024
|
* `duplicate` 不降权(两条内容相同,降谁都会丢信息);`dismissed` 一律不降权。 */
|
|
2014
2025
|
conflictDowngradeMap() {
|
|
2015
2026
|
const m = new Map();
|
|
@@ -2055,7 +2066,7 @@ export class MemoryStore {
|
|
|
2055
2066
|
* 已归档会话 id 集合(2026-09-21,D9-b 用)。
|
|
2056
2067
|
*
|
|
2057
2068
|
* 归档 = 主人主动封存、不再参与日常召回的那批会话。矛盾扫描必须排除它们,
|
|
2058
|
-
* 否则会拿几个月前已封存的对话互相判"矛盾",把待复盘队列塞满噪音(
|
|
2069
|
+
* 否则会拿几个月前已封存的对话互相判"矛盾",把待复盘队列塞满噪音(用户实测反馈)。
|
|
2059
2070
|
* 一次取全量而非逐行查 session_meta —— scan 里要判上千条,逐行查是 O(n) 次 SQL。
|
|
2060
2071
|
*/
|
|
2061
2072
|
archivedConvIdSet() {
|
|
@@ -2230,11 +2241,11 @@ export class MemoryStore {
|
|
|
2230
2241
|
if (!body) return { ok: false, reason: 'empty-text' };
|
|
2231
2242
|
const veinId = String(row.vein_id || '');
|
|
2232
2243
|
if (!veinId) return { ok: false, reason: 'no-vein' };
|
|
2233
|
-
// ── A-01(1.5.1
|
|
2244
|
+
// ── A-01(1.5.1 红蓝对抗;用户 2026-09-25 裁决「备份并覆盖」)──────────────────
|
|
2234
2245
|
// 采纳写入的键是 `(source='vein', conv_id=<主脉 id>)` ⇒ **同一主脉的两条候选先后采纳
|
|
2235
2246
|
// 会命中同一个 ON CONFLICT,后一条静默吃掉前一条**(无留痕、无归档 —— 数据真的没了;
|
|
2236
2247
|
// 红队 A 与红队 E 各自独立复现)。
|
|
2237
|
-
//
|
|
2248
|
+
// 用户的裁决与理由:主脉记忆是一种**概括**,它理应随主脉本身一起成长 ⇒ **覆盖**是对的;
|
|
2238
2249
|
// 但覆盖必须能回头 ⇒ 动手前把旧版**归档落盘**,归档失败就**拒绝覆盖**
|
|
2239
2250
|
// (与 forget 那条"归档失败绝不打标记"同一条纪律),并把结果回传调用方去记总变动日志。
|
|
2240
2251
|
let replaced = null;
|
|
@@ -2626,6 +2637,26 @@ export class MemoryStore {
|
|
|
2626
2637
|
return { ok: true, id: String(oldId), by: String(newId) };
|
|
2627
2638
|
}
|
|
2628
2639
|
|
|
2640
|
+
/** **作废**一条结论(2026-10-15,用户拍板「允许作废一条结论」)。
|
|
2641
|
+
* 与 supersede 的分工:取代 = "有一条新的接着";作废 = "这条不成立了、没有接替者"。
|
|
2642
|
+
* 实现**不删行**(老规矩「只追不替」):只把 `superseded_by` 标成哨兵 `retracted`。
|
|
2643
|
+
* WHY 落 kv:`deep.retract:<id>` = `{reason, by, at}` —— 过一阵要回答"当初为什么不要它"时,
|
|
2644
|
+
* 答案必须在库里,而不在某次已经折叠掉的会话里。
|
|
2645
|
+
* ⚠️ 已被取代/已作废的条目**再作废一次会被拒**(`already-retired`)——
|
|
2646
|
+
* 否则后一次的 reason 会把前一次的盖掉,留痕就变成"最后一次说的算"。 */
|
|
2647
|
+
retractDeepItem(id, { reason = '', by = 'user' } = {}) {
|
|
2648
|
+
const row = this.deepItemById(id);
|
|
2649
|
+
if (!row) return { ok: false, reason: 'not-found' };
|
|
2650
|
+
const prev = String(row.superseded_by || '');
|
|
2651
|
+
if (prev !== '') return { ok: false, reason: 'already-retired', superseded_by: prev };
|
|
2652
|
+
this.db.prepare('UPDATE deep_item SET superseded_by=?, updated_at=? WHERE id=?')
|
|
2653
|
+
.run(DEEP_RETRACTED, utcIso(), String(id));
|
|
2654
|
+
try {
|
|
2655
|
+
this.kvSet('deep.retract:' + String(id), JSON.stringify({ reason: String(reason), by: String(by), at: utcIso() }));
|
|
2656
|
+
} catch { /* 留痕失败**不改变**"作废已生效"这件事 —— 表已经改了,回执不许说谎 */ }
|
|
2657
|
+
return { ok: true, id: String(id), superseded_by: DEEP_RETRACTED, reason: String(reason) };
|
|
2658
|
+
}
|
|
2659
|
+
|
|
2629
2660
|
/** 加固 +1。⚠️ **只该由"真被采用"调用** —— 注入不算(见列注释:注入就加分会形成自我强化回路)。
|
|
2630
2661
|
* 采用信号的首选 = `recall` 取过它指向的原文(那条路已在投递留痕面内)。 */
|
|
2631
2662
|
bumpDeepHit(id) {
|
|
@@ -2644,6 +2675,29 @@ export class MemoryStore {
|
|
|
2644
2675
|
return Number(this.db.prepare('SELECT COUNT(*) AS n FROM deep_item').get()?.n) || 0;
|
|
2645
2676
|
}
|
|
2646
2677
|
|
|
2678
|
+
/** 已退役(被取代或已作废)的条数 —— 与 deepItemCount 分开报:
|
|
2679
|
+
* "库里 411 条"和"现在还会被挑的是 408 条"是两件事,混着说会让人以为退役的还在参与。 */
|
|
2680
|
+
deepRetiredCount() {
|
|
2681
|
+
return Number(this.db.prepare("SELECT COUNT(*) AS n FROM deep_item WHERE superseded_by<>''").get()?.n) || 0;
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
/** 条目行 → **对外视图**(面板、工具、HTTP 三面共用一份)。
|
|
2685
|
+
* WHY 落在这里:api.js 的 `/deep/items` 与 tools.js 的 `list` 报的是**同一种东西**,
|
|
2686
|
+
* 各写一份映射必然漂(本仓反复吃过这个亏)⇒ 单一来源钉在库这一层,两个面都调它。
|
|
2687
|
+
* 故意**不**放 `db` 内部行:`superseded_by` 这种内部列也照发(面板要能看出"它退役了"),
|
|
2688
|
+
* 但字段名统一成稳定的一套,免得前端跟着列名漂。 */
|
|
2689
|
+
deepItemView(r) {
|
|
2690
|
+
const row = r || {};
|
|
2691
|
+
return {
|
|
2692
|
+
id: String(row.id ?? ''), kind: String(row.kind ?? ''), text: String(row.text ?? ''),
|
|
2693
|
+
durability: String(row.durability ?? ''),
|
|
2694
|
+
hit_count: Number(row.hit_count) || 0, pinned: Number(row.pinned) || 0,
|
|
2695
|
+
conv_id: String(row.conv_id ?? ''), src: String(row.src ?? ''), at: String(row.at ?? ''),
|
|
2696
|
+
superseded_by: String(row.superseded_by ?? ''), branch_id: String(row.branch_id ?? ''),
|
|
2697
|
+
created_at: String(row.created_at ?? ''),
|
|
2698
|
+
};
|
|
2699
|
+
}
|
|
2700
|
+
|
|
2647
2701
|
// ---- raw turns (DSH incremental capture, passive) ----
|
|
2648
2702
|
/** 追加一条原文轮次(幂等:(session_id,seq) 冲突时更新正文)。
|
|
2649
2703
|
* v14:`source` 记**这条原文属于哪个来源**(取值口径见 RAW_SOURCE_DSH 段)。缺省 null ⇒ 按 id
|
package/lib/host/persona.js
CHANGED
|
@@ -63,8 +63,8 @@ export const DEFAULT_SETTINGS = Object.freeze({
|
|
|
63
63
|
},
|
|
64
64
|
memory: {
|
|
65
65
|
l1Enabled: true,
|
|
66
|
-
l1BudgetTokens: 720, // 1.6 步 5:1200 → 720(「瘦身到现在的 60
|
|
67
|
-
l1MaxItems: 10, // 1.6:8 → 10(
|
|
66
|
+
l1BudgetTokens: 720, // 1.6 步 5:1200 → 720(「瘦身到现在的 60%」,用户 2026-10-04 拍)
|
|
67
|
+
l1MaxItems: 10, // 1.6:8 → 10(用户 2026-10-04:「8 条也可以考虑改 10 条,其中 2 条作为最硬的事实的席位」)
|
|
68
68
|
l1MaxLineChars: 180, // 兜底硬顶:只对病态长标题生效(截断优先落在句末)
|
|
69
69
|
l1SummaryChars: 110, // 摘要段预算:第一句超此长度才硬截并标 …(2026-09-16 与用户定案)
|
|
70
70
|
categoryWeights: { // mode -> 领域类别权重(D6,先验倾向)
|
|
@@ -105,6 +105,17 @@ export const DEFAULT_SETTINGS = Object.freeze({
|
|
|
105
105
|
baseUrl: '', // 空 = 落到环境变量 DSH_LING_ASSISTANT,再落到内置默认(loopback:11434)
|
|
106
106
|
model: '', // 空 = 落到环境变量 DSH_LING_ASSISTANT_MODEL,再落到 qwen3.5:9b
|
|
107
107
|
},
|
|
108
|
+
// 后台自动行为(2026-10-16 与用户定):这些行为**一直是开着的**,只是此前既没有文档、
|
|
109
|
+
// 也没有面板开关 —— 只能靠改插件配置(cfg)或读源码才知道。现在收进 settings.auto:
|
|
110
|
+
// 面板可读写,默认值与原硬编码**逐字相同**(关掉前请听清每行说明的后果)。
|
|
111
|
+
// 三级优先级:settings.auto.<键> > 插件配置 cfg.<旧键> > 这里(解析只在 deepsummary.resolveAuto 一处)。
|
|
112
|
+
auto: {
|
|
113
|
+
deepSummary: true, // 自动深摘总闸:关掉后只有你点「深摘」时才调用模型;已经深摘过的会话不受影响
|
|
114
|
+
deepIntervalMin: 30, // 自动深摘间隔(分钟,夹到 10..1440):调小 = 记忆更新更勤、更吃模型额度;调大 = 省额度
|
|
115
|
+
deepMaxPerRun: 2, // 每轮最多深摘几个会话(夹到 1..20):调大 = 一轮更久、一次花掉更多额度
|
|
116
|
+
bootProbeDelayMs: 12000, // 启动后多久做一次 LLM 探活(毫秒,夹到 0..600000):0 = 立即探活(宿主刚起时可能失败)
|
|
117
|
+
feedbackAuto: true, // 反馈成长回路(事件驱动):关掉后点踩/点赞不再自动长出规矩与习惯提议
|
|
118
|
+
},
|
|
108
119
|
dates: [], // 自定义纪念日 [{m,d,label,greeting}](内置 9-07 新生纪念日见 clock.js)
|
|
109
120
|
});
|
|
110
121
|
|
package/lib/host/rawlog.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// dsh-ling host — 直通原文(1.6 · P2 第一优先)。
|
|
2
2
|
//
|
|
3
|
-
// 为什么要有这个模块(2026-10-04
|
|
3
|
+
// 为什么要有这个模块(2026-10-04 用户定「第一优先级就是先把读原文的这个事儿给办了」):
|
|
4
4
|
// 器灵此前**没有任何正规通道**能读会话原文 —— 7 个工具里一个都读不到,
|
|
5
5
|
// 而 memory.db 的 `dsh_turns_raw` 是**残的**(本会话实测:库里 13 条 vs 磁盘 473 条,≈2.7%)。
|
|
6
6
|
// 真全文只躺在磁盘上: ~/.dsh/sessions/<项目目录>/<会话目录>/session.v4.jsonl.zstd
|
|
@@ -97,13 +97,29 @@ export function searchDeepItems(memory, q, limit = SEARCH_DEEP_MAX) {
|
|
|
97
97
|
const words = String(q || '').trim().split(/\s+/).map((w) => w.replace(/[%_\\]/g, '')).filter(Boolean);
|
|
98
98
|
if (!words.length) return [];
|
|
99
99
|
try {
|
|
100
|
-
|
|
100
|
+
// ⚠️ 2026-10-06:**类目名(kind)也在检索面里** —— 修前只扫 `text`,于是查询里的类目词打不中
|
|
101
|
+
// 它们自己的条目:实测搜「纪律」时那 18 条只回来 2 条(恰好正文含"纪律"二字的那些),
|
|
102
|
+
// 而**同族缺陷对"承诺 / 决定 / 关系 / 事实 / 偏好"一样成立**。
|
|
103
|
+
// 多词仍是 **AND**(每词都得在 text 或 kind 里出现其一),与宿主检索面同解,不另立口径。
|
|
104
|
+
const where = words.map(() => "(text LIKE ? ESCAPE '\\' OR kind LIKE ? ESCAPE '\\')").join(' AND ');
|
|
105
|
+
const args = [];
|
|
106
|
+
for (const w of words) { args.push(`%${w}%`, `%${w}%`); }
|
|
107
|
+
args.push(Math.max(1, Math.min(50, Number(limit) || SEARCH_DEEP_MAX)));
|
|
101
108
|
return db.prepare(
|
|
102
109
|
`SELECT id, kind, text, conv_id, src, seq_from, seq_to, pinned, hit_count FROM deep_item
|
|
103
110
|
WHERE superseded_by='' AND ${where}
|
|
104
111
|
ORDER BY pinned DESC, hit_count DESC, created_at DESC LIMIT ?`,
|
|
105
|
-
).all(...
|
|
106
|
-
} catch {
|
|
112
|
+
).all(...args);
|
|
113
|
+
} catch (e) {
|
|
114
|
+
// ── F3(1.7 修复批次,2026-10-15)─────────────────────────────────────────────
|
|
115
|
+
// 修前是 `catch { return []; }` —— 把**库读失败**(损坏 / 被占用 / SQL 错)归并成**零命中**,
|
|
116
|
+
// 调用方读到的就是"库里没有",而真实情况是"我们自己的库读不出来"。违反本项目口径
|
|
117
|
+
// 「**库不可用 ≠ 零命中**」(同文件对宿主那条路一直严格分开:`search-unavailable` / 超时文案)。
|
|
118
|
+
// 现在**只吞"表 / 列不存在"** —— 那是"这张表还没建"的正常态;其余一律往上冒,由工具层
|
|
119
|
+
// 转成 `local-unavailable`(见 tools.js 的 runSearch)。
|
|
120
|
+
if (/no such table|no such column/i.test(String(e?.message ?? e))) return [];
|
|
121
|
+
throw e;
|
|
122
|
+
}
|
|
107
123
|
}
|
|
108
124
|
|
|
109
125
|
/** 检索档本地原文层一次最多带出几轮(默认同 `SEARCH_HITS_DEFAULT`)。 */
|
|
@@ -121,6 +137,8 @@ export const SEARCH_LOCAL_TURNS_MAX = 50;
|
|
|
121
137
|
*
|
|
122
138
|
* 与 `searchDeepItems` 同解的三条(不另立口径):多词 = **AND**;词里的 `% _ \` 先剥掉;
|
|
123
139
|
* 表不存在时**静默返回空**(检索面不该因为某张表缺席而整体失败)。
|
|
140
|
+
* ⚠️ F3(1.7 修复批次)把这条**收窄**了:静默只对"**表 / 列不存在**"这一类;
|
|
141
|
+
* 库损坏 / 被占用 / SQL 错**往上冒** —— 库不可用 ≠ 零命中,两者的下一步动作完全不同。
|
|
124
142
|
*
|
|
125
143
|
* @param {object} memory MemoryStore
|
|
126
144
|
* @param {string} q 查询词(空格分隔 = 同时满足)
|
|
@@ -143,7 +161,12 @@ export function searchLocalTurns(memory, q, limit = SEARCH_HITS_DEFAULT, { selfI
|
|
|
143
161
|
`SELECT session_id, seq, role, ts, text FROM dsh_turns_raw
|
|
144
162
|
WHERE ${where} ORDER BY ts DESC LIMIT ?`,
|
|
145
163
|
).all(...words.map((w) => `%${w}%`), cap);
|
|
146
|
-
} catch
|
|
164
|
+
} catch (e) {
|
|
165
|
+
// F3(1.7 修复批次):同 `searchDeepItems` —— **只吞"表 / 列不存在"**(表缺席是正常态),
|
|
166
|
+
// 库损坏 / 被占用 / SQL 错必须往上冒(往上冒才会被渲染成"库读不出来",而不是"没找到")。
|
|
167
|
+
if (/no such table|no such column/i.test(String(e?.message ?? e))) return { rows: [], selfExcluded: 0 };
|
|
168
|
+
throw e;
|
|
169
|
+
}
|
|
147
170
|
const self = String(selfId || '');
|
|
148
171
|
const mapped = rows.map((r) => {
|
|
149
172
|
const id = String(r.session_id ?? '');
|
|
@@ -269,15 +292,29 @@ export function renderSearch(v) {
|
|
|
269
292
|
+ ' 这不是"库里没有",是**通道没接上** —— 按内容找会话这条路暂时走不通,'
|
|
270
293
|
+ '可改用**不带参数**的 recall() 按时间翻清单。',
|
|
271
294
|
'search-failed': `检索报错,这次没搜成:${msg}。换个更短的词再试(中文 2~4 字命中最好)。`,
|
|
295
|
+
// F3(1.7 修复批次):**新开**一个理由词,不复用 `search-unavailable` ——
|
|
296
|
+
// 那句写的是"**宿主**检索面不可用",而这一条是"**我们自己的库**读不出来",
|
|
297
|
+
// 套用就是把假话换了个地方说(两条路该采取的动作也不同:前者换通道,后者查库/看占用)。
|
|
298
|
+
'local-unavailable':
|
|
299
|
+
`本机库读不出来,这次没搜成。原因:${msg}`
|
|
300
|
+
+ ' **这不是"库里没有",是我们自己的库读不出来** —— 条目层与原文层都在这个库里,'
|
|
301
|
+
+ '这次两半都没拿到(所以别据此断定"没发生过")。'
|
|
302
|
+
+ '可先改用**不带参数**的 recall() 按时间翻清单;若反复如此,'
|
|
303
|
+
+ '多半是库文件损坏或被别的进程占着(库路径见启动日志的 dataDir)。',
|
|
272
304
|
}[v?.reason];
|
|
273
305
|
return why || `未搜到内容(${v?.reason || '未知'})。`;
|
|
274
306
|
}
|
|
275
307
|
const scope = v.all
|
|
276
308
|
? '**未限定范围**(all=true —— 含子代理等非主人会话)'
|
|
277
|
-
: (v.
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
309
|
+
: (v.localScope
|
|
310
|
+
// ⚠️ 本地路**没有可见集过滤**(2026-10-06 外部文档核对抓到):它直查 `dsh_turns_raw` ——
|
|
311
|
+
// 那是**全量**(实测 11,296 行 / 1,647 会话,其中 `source='dsweb'` 的 1,523 个网页端会话、
|
|
312
|
+
// 未标来源的 104 个)⇒ **不许印"已在主人自己的会话内"**,那是说假话。
|
|
313
|
+
? '⚠️ **本机库全量**(这条路不过 `listSessions` 可见集:含网页端导入的会话与未标来源的会话)'
|
|
314
|
+
: (v.degraded
|
|
315
|
+
? '⚠️ **没能限定范围** —— 宿主这次没给 `listSessions`,所以下面这份结果**可能含子代理或其它会话**,'
|
|
316
|
+
+ '不是"只搜主人自己的会话"(范围限制这条通道暂时失效,不是"库里只有这些")'
|
|
317
|
+
: '已在「**主人自己的会话**」内(默认范围)'));
|
|
281
318
|
// ⚠️ **原文来源必须看得出来是哪一个**(2026-10-05 夜 · 主人拍板方案 A):
|
|
282
319
|
// 两条路覆盖面**不一样**,不说清就会被读成"库里只有这些"。
|
|
283
320
|
// · 本地(默认):库内原文层 —— 只含"真人消息 + 有正文的回复"(capture 侧就这么收的),
|
package/lib/host/snapshot.js
CHANGED
|
@@ -93,7 +93,8 @@ export function buildSnapshotText(ctx, memory, settings, sessionId, { keywords =
|
|
|
93
93
|
// 下一轮照样进候选;与 §3 开关"只管渲染、不管交代记账"同一取向)。
|
|
94
94
|
const logLines = Array.isArray(d?.lines) ? d.lines : [];
|
|
95
95
|
const logDetailLines = logLines.filter((x) => !BRANCH_LOG_FOLD_LINE.test(String(x)));
|
|
96
|
-
// §3 独立开关:只管**树改动播报**(
|
|
96
|
+
// §3 独立开关:只管**树改动播报**(与 `settings.persona.enabled` **并列**的一道闸 ——
|
|
97
|
+
// 外层 `cfg.injectEnabled` 关掉的是**整个记忆段**,见 inject.js:registerMemorySection)。
|
|
97
98
|
// 关掉时:不成文、也不推进"已示人/已处置"水位(否则关着关着就把一段内容标成已读 ⇒
|
|
98
99
|
// 再打开时那段内容永远看不到)。`.last` 照走 —— 它是复盘退出判据,不是"已示人"判据:
|
|
99
100
|
// **开关只管"渲染",不管"交代记账"**(停推进会卡死复盘模式的退出,回归见 tree 套 9-5)。
|
package/lib/host/surgery.js
CHANGED
|
@@ -13,9 +13,11 @@
|
|
|
13
13
|
// 1.2 每启动票据 = $DSH_HOME/cache/dsh-ling/surgery.ticket(32 字节随机),只对**本机直连**(无代理头且带 UA)的请求下发,
|
|
14
14
|
// 每启动轮换(旧票即废);客户端首次手术前取一次并缓存,随后带 `x-dsh-ling-surgery: <ticket>`。
|
|
15
15
|
// 1.3 门下沉为一处 requireSurgery(headers, body, opts),覆盖全部 **14** 条人格/规矩/习惯写面。
|
|
16
|
-
// ⚠️ 条数在 2026-10-05 夜由 12 订正为 14(B-5):下方的覆盖面清单**逐条列了 14
|
|
17
|
-
// `api.js`
|
|
18
|
-
//
|
|
16
|
+
// ⚠️ 条数在 2026-10-05 夜由 12 订正为 14(B-5):下方的覆盖面清单**逐条列了 14 行**,那才是有权威的那份。
|
|
17
|
+
// 当时本行与 `api.js` 的门下沉注释还写着"12" ⇒ **两处都在说谎**。2026-10-16 复核:`api.js` 那侧
|
|
18
|
+
// 已全部对齐到 14(`api.js:541/603/2154/…/3231` 逐处都是 14)⇒ 本句现在只是在解释历史,
|
|
19
|
+
// **不再是当下的口径指控**(别再照它去找 api.js 的"12")。
|
|
20
|
+
// 口径不变:**条数一律以清单行数为准**,改清单时必须同批改这里与 api.js 那一处。
|
|
19
21
|
// 1.4 事件留痕 = $DSH_HOME/logs/surgery-YYYY-MM.jsonl,append-only + 整行哈希链(改一行即断链)。
|
|
20
22
|
// 1.5 逃生开关:DSH_LING_GUARD=off 或 settings.guard.enforce === false ⇒ 整体放行(绝不把主人锁在门外)。
|
|
21
23
|
//
|
|
@@ -34,7 +36,7 @@ import { join } from 'node:path';
|
|
|
34
36
|
import { dshHome, defaultDataDir } from './util.js';
|
|
35
37
|
import { headerOf, realPeerAddress } from './guard.js';
|
|
36
38
|
|
|
37
|
-
// ── 覆盖面清单:
|
|
39
|
+
// ── 覆盖面清单:14 条能改人格/规矩/习惯的写面(行号 = 2026-09-30 深夜实测,改 api.js 后须复核)──
|
|
38
40
|
// # 端点 register 行 "原"过门情况(改前) 改后
|
|
39
41
|
// 1 /persona 1809 仅 wasSealed && !wantLock(wantLock 可绕过) 过门
|
|
40
42
|
// 2 /persona/check 1855 无门(只读验钥 —— 但是一把**验句预言机**) 过门
|