@furongjun1999/dsh-memory 0.7.1 → 0.7.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/README.md +58 -48
- package/codebuddy/CODEBUDDY.md +9 -9
- package/docs/README.md +148 -144
- package/docs/discipline/harnesses.yaml +16 -8
- package/docs/discipline/templates/zcode-user.md.tmpl +49 -0
- package/docs/eval/AGI/344/270/203/347/273/264/350/257/204/345/210/206/346/212/245/345/221/212_/345/205/255/345/256/266/350/256/260/345/277/206/347/263/273/347/273/237/345/220/214/345/260/272/345/256/236/346/265/213_v3.1.md +340 -0
- package/docs/eval/N225_/347/264/242/345/274/225/346/227/245/345/277/227/351/235/236/345/257/271/350/261/241/350/243/205/350/275/275/351/235/242/347/261/273/345/236/213/351/227/270_v1.0.md +2 -2
- package/docs/eval/cons200_/345/206/262/347/252/201/346/243/200/346/265/213/351/200/211/351/235/242_/345/256/236/346/226/275/350/256/260/345/275/225_v1.0.md +1 -1
- package/docs/eval/cons200_/345/206/262/347/252/201/346/243/200/346/265/213/351/200/211/351/235/242_/345/256/236/346/226/275/350/256/260/345/275/225_v1.1.md +1 -1
- package/docs/eval/issue43_/351/273/230/350/256/244/347/255/226/347/225/245/344/270/216/351/224/256/347/261/273/345/236/213/351/227/270_v1.1.md +1 -1
- package/docs/eval/issue43_/351/273/230/350/256/244/347/255/226/347/225/245/345/212/240/350/275/275/344/270/216/345/207/255/346/215/256/346/230/216/346/226/207/351/230/237/345/210/227_v1.0.md +2 -2
- package/docs/eval/issue50_/345/205/203/346/225/260/346/215/256/351/200/217/344/274/240/344/270/216/345/205/234/345/272/225_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +5 -5
- package/docs/eval/issue50_/345/215/212/351/207/215/345/244/215/345/276/205/345/256/232/345/244/215/346/240/270_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +9 -9
- package/docs/eval/issue50_/345/276/205/345/256/232/345/244/215/346/240/270/345/205/245/351/230/237_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +4 -4
- package/docs/eval/issue50_/351/207/215/350/246/201/345/272/246/345/220/214/346/272/220/344/270/216/344/277/235/346/212/244/350/257/255/344/271/211_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +1 -1
- package/docs/eval/issue51_/344/270/200/351/224/256/345/256/211/350/243/205/345/244/261/350/264/245_/345/275/222/345/261/236/345/210/244/345/256/232_v1.0.md +1 -1
- package/docs/eval/issue52_/346/235/241/344/273/266/345/205/210/350/241/214/344/270/216/346/210/252/346/226/255/345/217/257/350/247/202/346/265/213_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +3 -3
- package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/242/345/217/230/346/233/264/345/215/225/344/270/216/345/233/236/346/273/232/345/216/237/350/257/255_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +3 -3
- package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/243/345/207/206/345/205/245/350/257/273/346/225/260_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +7 -7
- package/docs/eval//344/274/230/345/214/226/347/254/254/344/270/200/346/211/271_/346/216/245/347/272/277/344/270/216/347/255/211/344/273/267/345/217/230/346/215/242_v1.0.md +2 -2
- package/docs/eval//344/274/230/345/214/226/347/254/254/344/270/211/346/211/271_/351/227/250/347/246/201/350/275/254/346/255/243/344/270/216/350/260/203/345/272/246/346/255/242/350/241/200/344/270/216/351/227/250/346/216/247/346/224/266/345/217/243_v1.0.md +1 -1
- package/docs/eval//344/274/230/345/214/226/347/254/254/344/272/214/346/211/271_/344/276/235/350/265/226/351/200/217/344/274/240/344/270/216/345/257/271/346/213/215/345/217/243/345/276/204/344/270/216/350/264/237/347/274/223/345/255/230_v1.0.md +7 -7
- package/docs/eval//345/207/272/350/264/247/351/235/242/345/206/222/347/203/237_/350/277/233/350/264/247/351/227/250/347/246/201_v1.0.md +10 -10
- package/docs/eval//345/217/221/345/270/20307_/345/244/226/351/203/250/346/212/245/345/221/212/345/233/233/346/211/271/344/277/256/345/244/215/344/270/216/346/217/222/344/273/266/351/235/242/345/212/240/345/233/272_v1.0.md +4 -0
- package/docs/eval//345/217/221/345/270/20308_/350/207/252/350/277/255/344/273/243/344/270/216/347/235/241/347/234/240_/345/233/276/346/243/200/347/264/242/350/267/257/344/270/216/346/235/203/351/207/215_v1.0.md +1 -1
- package/docs/eval//345/217/221/345/270/20309_/346/243/200/347/264/242/351/235/242/344/270/211/346/211/271/346/224/266/345/217/243_v1.0.md +1 -1
- package/docs/eval//345/217/221/345/270/20310_/350/257/273/351/235/242/346/215/237/345/235/217UTF8/345/256/266/346/227/217/346/224/266/345/217/243_v1.0.md +71 -0
- package/docs/eval//345/217/221/345/270/20311_/345/244/226/346/212/245/345/205/255/350/277/236/344/277/256/344/270/216/344/270/226/347/225/214/346/250/241/345/236/213/345/272/225/345/272/247_v1.0.md +91 -0
- package/docs/eval//345/275/222/344/270/200/345/261/202/347/274/272/347/234/201/347/277/273/345/205/263_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +2 -2
- package/docs/eval//347/224/250/346/210/267/350/243/201/345/206/263/345/244/204/347/275/256_/347/254/254/344/270/200/346/211/271_v1.0.md +195 -0
- package/docs/eval//347/224/250/346/210/267/350/243/201/345/206/263/345/244/204/347/275/256_/347/254/254/344/272/214/346/211/271B6_/346/234/254/346/234/272/350/267/257/345/276/204/347/233/270/345/257/271/345/214/226_v1.0.md +78 -0
- package/docs/eval//347/253/257/345/210/260/347/253/257LoCoMoQA/345/220/214/345/217/243/345/276/204/345/257/271/347/205/247_v1.0.md +1 -1
- package/docs/eval//347/253/257/345/210/260/347/253/257/345/271/262/346/211/260/346/261/240/350/257/204/346/265/213_/347/241/256/345/256/232/346/200/247/350/243/201/345/206/263vsLLM_judge_v1.1.md +3 -3
- package/docs/eval//347/254/2543/345/261/202stg/347/273/223/346/236/204/347/264/242/345/274/225_/345/256/236/346/226/275/350/256/260/345/275/225_v1.0.md +1 -1
- package/docs/eval//347/274/226/347/240/201/351/235/242/345/211/215/347/275/256_/345/205/245/345/217/243/350/207/252/344/277/235/350/257/201UTF8/344/270/216/346/226/207/346/234/254open/345/256/210/345/215/253_v1.0.md +1 -1
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v26.md +196 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v27.md +259 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v28.md +217 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v29.md +270 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v30.md +231 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v31.md +281 -0
- package/docs/hive//345/256/211/345/205/250/345/256/241/350/256/241/345/256/236/351/224/232_v0.1.md +1 -1
- package/docs/hive//350/234/202/345/267/242M6_ingest/345/256/236/346/226/275/350/256/241/345/210/222_v0.1.md +1 -1
- package/docs/hive//350/234/202/345/267/242/345/217/214/345/256/236/344/276/213/344/272/222/351/252/214_/350/256/276/350/256/241/345/256/232/347/250/277.md +1 -1
- package/docs/hive//350/234/202/345/267/242/350/256/276/350/256/241_/347/220/206/350/256/272/345/257/271/351/275/220_v0.1.md +17 -17
- package/docs/hive//350/234/202/345/267/242/350/277/255/344/273/243_/345/256/217/350/247/202/344/270/216/347/276/244/344/275/223/350/260/203/345/272/246_v0.1.md +1 -1
- package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +2 -2
- package/docs/mdcg//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +42 -41
- package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +29 -8
- package/docs/mdcg//347/235/241/347/234/240/345/221/250/346/234/237_/350/277/220/347/273/264/345/211/215/346/217/220/344/270/216/347/273/264/346/212/244/346/214/207/345/215/227_v1.0.md +71 -1
- package/docs/plans//345/205/250/344/270/255/346/226/207/347/274/226/347/240/201/344/270/216/350/234/202/345/267/242/344/273/273/345/212/241/346/240/207/350/257/206/345/245/221/347/272/246_v2.0.md +1 -1
- package/docs/plans//345/244/232/344/270/273/344/275/223/344/270/226/347/225/214/346/250/241/345/236/213_/345/257/271/351/275/220/350/257/204/344/274/260/344/270/216/350/220/275/345/234/260/350/256/276/350/256/241_v0.1.md +141 -0
- package/docs/plans//347/235/241/347/234/240/344/270/216/350/207/252/350/277/255/344/273/243_/345/212/237/350/203/275/344/274/230/345/214/226/350/256/276/350/256/241_v0.4.md +2 -2
- package/docs/plans//350/257/255/344/271/211/346/227/266/347/251/272/345/233/276/350/241/245/345/205/250_/344/270/226/347/225/214/346/250/241/345/236/213/345/212/237/350/203/275/347/253/257_/350/256/276/350/256/241_v0.1.md +107 -0
- package/docs//345/267/245/344/275/234/347/272/252/345/276/213_/350/256/244/347/237/245/345/233/276/346/235/241/347/233/256_v1.1.json +9 -8
- package/docs//347/247/244_/350/256/260/345/277/206/347/263/273/347/273/237/350/257/204/346/265/213/350/247/204/350/214/203_v2.1.md +225 -0
- package/dsh/cordis-patch-profile-web.example.yml +2 -2
- package/lib/bridge.js +9 -0
- package/lib/hooks.d.ts +8 -1
- package/lib/hooks.js +94 -32
- package/lib/lib/hook_audit.d.ts +55 -0
- package/lib/lib/hook_audit.js +208 -0
- package/lib/lib/mutual.js +5 -1
- package/lib/lib/roleplay_web.js +17 -2
- package/md_cg/auditview.py +248 -0
- package/md_cg/bench6_arms.py +10 -6
- package/md_cg/bench_e2e_judge.py +4 -2
- package/md_cg/branches.py +29 -3
- package/md_cg/census.py +2 -1
- package/md_cg/consistency.py +38 -13
- package/md_cg/datapath.py +38 -11
- package/md_cg/forgetting.py +15 -2
- package/md_cg/freshness.py +32 -11
- package/md_cg/ghostref.py +130 -0
- package/md_cg/judgment_manifest.py +7 -0
- package/md_cg/mcp_server.py +58 -1
- package/md_cg/mdcg.py +4923 -4764
- package/md_cg/mdcos.py +40 -15
- package/md_cg/mreview/locate.py +1 -1
- package/md_cg/nodefile.py +77 -13
- package/md_cg/readcache.py +29 -4
- package/md_cg/rollback.py +12 -3
- package/md_cg/routing.py +10 -1
- package/md_cg/selfreport.py +12 -3
- package/md_cg/sleep.py +129 -13
- package/md_cg/sources.py +1 -1
- package/md_cg/state_events.py +131 -0
- package/md_cg/sustain.py +1436 -1436
- package/md_cg/test_auditview.py +277 -0
- package/md_cg/test_autonomy_modes.py +2 -1
- package/md_cg/test_boot_openblas_guard.py +197 -0
- package/md_cg/test_ccg_form_parity.py +95 -4
- package/md_cg/test_cons200_scan_selection.py +4 -2
- package/md_cg/test_corrupt_utf8_read_surfaces.py +451 -0
- package/md_cg/test_datapath_device_name.py +62 -3
- package/md_cg/test_generation_guard.py +20 -11
- package/md_cg/test_ghostref.py +182 -0
- package/md_cg/test_health_corrupt_utf8.py +37 -0
- package/md_cg/test_index_crossprocess_reload.py +39 -24
- package/md_cg/test_interop.py +1 -1
- package/md_cg/test_issue52_scan_condition_first.py +1 -1
- package/md_cg/test_issue53_cipher_health_hash.py +385 -0
- package/md_cg/test_lock.py +16 -7
- package/md_cg/test_m3_h9_semantic_guard.py +1 -1
- package/md_cg/test_n230_dirty_replay.py +375 -375
- package/md_cg/test_n238_body_text_parity.py +156 -0
- package/md_cg/test_n276_probe_hoist.py +199 -0
- package/md_cg/test_p4_freshness.py +95 -1
- package/md_cg/test_policy_required_ccg.py +3 -1
- package/md_cg/test_readcache_prodpath.py +66 -0
- package/md_cg/test_sleep.py +58 -1
- package/md_cg/test_sleep_p1.py +218 -2
- package/md_cg/test_state_events.py +178 -0
- package/md_cg/test_time_core_lint.py +8 -1
- package/md_cg/test_writepipe.py +8 -5
- package/md_cg/tokens.py +11 -2
- package/md_cg/writepipe.py +76 -0
- package/package.json +2 -2
- package/skills/plugin.json +1 -1
- package/src/bridge.ts +9 -0
- package/src/hooks.ts +576 -517
- package/src/lib/hook_audit.ts +243 -0
- package/src/lib/mutual.ts +5 -1
- package/src/lib/roleplay_web.ts +17 -2
- package/zcode/AGENTS.md +9 -9
package/lib/hooks.js
CHANGED
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
* `origin` / `delegationDepth`)的自动记忆**整条会话拦掉**;② **消息级**——
|
|
21
21
|
* `source.form === 'relay'`(「另一个 agent 发给本 agent 的消息」)不写。
|
|
22
22
|
* 两条判据均为「**字段在场且取值匹配才拦**」:字段缺失一律退化为不过滤(默认放行),
|
|
23
|
-
*
|
|
23
|
+
* 宿主字段面已在本机真实会话事件上观测(2026-10-05 订正:19 场,会话头
|
|
24
|
+
* `delegationDepth=0` 真实在写);**仍未观测**的是 `origin='subagent'` /
|
|
25
|
+
* `delegationDepth>0` / `form='relay'` 的真实出现(见 installMemoryHooks 内注释)。
|
|
24
26
|
*
|
|
25
27
|
* autoRecall:通过 system-prompt/assemble 事件(waterfall,异步允许)在每次
|
|
26
28
|
* 模型请求组装 system prompt 时自动注入灵枢最近记忆
|
|
@@ -43,6 +45,7 @@
|
|
|
43
45
|
import '@deepseek-ai/dsh-session';
|
|
44
46
|
import '@deepseek-ai/dsh-system-prompt';
|
|
45
47
|
import { escapePromptBraces, renderUntrustedMemoryBlock } from './lib/prompt_safety.js';
|
|
48
|
+
import { HookAuditRecorder, kindOf } from './lib/hook_audit.js';
|
|
46
49
|
/** 从 ContentBlock[] 提取纯文本。 */
|
|
47
50
|
function extractText(blocks) {
|
|
48
51
|
const parts = [];
|
|
@@ -88,8 +91,18 @@ export const WORD_CHARS = `A-Za-z0-9_${FW_DIGITS}${FW_UPPER}${FW_LOWER}_`;
|
|
|
88
91
|
/** 两宽数字类。 */
|
|
89
92
|
const DIGIT_CHARS = `0-9${FW_DIGITS}`;
|
|
90
93
|
/** 两宽凭据值类:词字符 + `@ # $ % ^ & * ! . -` 的两宽形态。
|
|
91
|
-
* ASCII 连字符一律转义(`\\-`)——`[_--]` 会被解析成 `_`→`-` 的**巨区间**。
|
|
92
|
-
|
|
94
|
+
* ASCII 连字符一律转义(`\\-`)——`[_--]` 会被解析成 `_`→`-` 的**巨区间**。
|
|
95
|
+
*
|
|
96
|
+
* N265(本轮补齐):半角 `&` 与全角 `#%^*` 此前缺位——其对照形态(全角 `&`、
|
|
97
|
+
* 半角 `# % ^ *`)早在类里,构成「同形不同过滤」:缺口字符落在值前 4 位内 ⇒ 整条漏检、
|
|
98
|
+
* 落在第 4 位之后 ⇒ 命中被截断在缺口处、尾部留明文。守卫
|
|
99
|
+
* test/fullwidth_redact.test.ts ⑧(形态一)与 ⑨(形态二)钉住这两只退化形态。
|
|
100
|
+
*
|
|
101
|
+
* ⚠️ 全角 `!` **不在**本类,且与半角 `!` 不对称(`!` 在类)——这是**有意保留**的现口径:
|
|
102
|
+
* 本文件「边界」段与同文件守卫 ④ 把全角叹号钉为句读边界(值在此收住)。
|
|
103
|
+
* 2026-10-05 使用者裁决:判 by-design——保留全角 ! 为句读边界,不得并入值类
|
|
104
|
+
* (并入必动守卫④),**勿在补字符集时顺手塞进来**——否则守卫 ④ 会红。 */
|
|
105
|
+
const CRED_VALUE_CHARS = `${WORD_CHARS}@@##$$%%^^&&**!.\\--.`;
|
|
93
106
|
/** 两宽令牌值类(本项目令牌 id/secret 的字符集:词字符 + `-`)。
|
|
94
107
|
* 导出供守卫核对字面量规则的同源性(同 ⑦),不承诺稳定 ABI。 */
|
|
95
108
|
export const TOKEN_VALUE_CHARS = `${WORD_CHARS}\\--`;
|
|
@@ -132,12 +145,27 @@ export const SENSITIVE_PATTERNS = [
|
|
|
132
145
|
+ `(?:${twoWidth('password')}|${twoWidth('passwd')}|${twoWidth('pwd')})`
|
|
133
146
|
+ `(?![${WORD_CHARS}])\\s*[:==:]\\s*[^\\s,,。;;]+`, 'gi'), label: '密码' },
|
|
134
147
|
{ re: new RegExp(`${twoWidth('Bearer')}\\s+[${BEARER_VALUE_CHARS}]{8,}`, 'gi'), label: '令牌' },
|
|
135
|
-
//
|
|
148
|
+
// 中文/日文密码标签:值限定非中文连续串(凭据特征),避免误伤「密码是重要的安全概念」;
|
|
136
149
|
// 分隔符补全角等号 `=`(半角 `=` 本就不在本规则的集合里,故只补全角形态)。
|
|
137
|
-
|
|
150
|
+
// N269:词形补齐——繁体「密碼」、日文「パスワード」、中文「口令」与「密码」同表同口径
|
|
151
|
+
//(三者此前整条漏检:界面承诺 :66「密码…默认过滤」,却认不出这三种常见写法);
|
|
152
|
+
// 日文 `は` 一类助词**刻意不并入分隔符集合**(`パスワードは重要です` 是普通句子,
|
|
153
|
+
// 并进去即刻误伤,与「值类不含中文」同一取舍)。
|
|
154
|
+
{ re: new RegExp(`(?:密码|密碼|口令|パスワード)\\s*[::是=]\\s*[${CRED_VALUE_CHARS}]{4,}`, 'g'), label: '密码' },
|
|
138
155
|
// 两宽:全角数字形态同样要被认(`\b` 换成两宽 lookaround,见上)
|
|
139
156
|
{ re: new RegExp(`(?<![${WORD_CHARS}])[${DIGIT_CHARS}]{17}[${DIGIT_CHARS}XxXx](?![${WORD_CHARS}])`, 'g'), label: '身份证号' },
|
|
140
157
|
{ re: new RegExp(`(?<![${WORD_CHARS}])[11][3-93-9][${DIGIT_CHARS}]{9}(?![${WORD_CHARS}])`, 'g'), label: '手机号' },
|
|
158
|
+
// N269:凭据标签词形的「标签: 值」形态——密钥 / token / secret / creds 此前整条漏检
|
|
159
|
+
//(界面承诺 :66「密钥/密码/令牌…默认过滤」,而裸 `token:`/`secret=`/`creds=` 一个都不认)。
|
|
160
|
+
// 值类取共享单点 CRED_VALUE_CHARS 并设 `{4,}` 下限(与中文密码规则同口径):比
|
|
161
|
+
// 「任意非空白续写」窄,避免吃掉 `token: 这句是说明文字` 一类普通文本;词形用
|
|
162
|
+
// twoWidth 单点生成(半角/全角拼写都认,`TOKEN:…` 同样命中);标签前后沿用
|
|
163
|
+
// 两宽 lookaround(`mytoken=`/`tokenize:` 不命中)。
|
|
164
|
+
// 位置约束:两条令牌规则必须留在数组**最后两位**(test/token_redact_parity.test.ts
|
|
165
|
+
// 的交换序按 n-2/n-1 取它们做对拍),故本规则插在其前。
|
|
166
|
+
{ re: new RegExp(`(?<![${WORD_CHARS}])`
|
|
167
|
+
+ `(?:密钥|${twoWidth('token')}|${twoWidth('secret')}|${twoWidth('creds')})`
|
|
168
|
+
+ `(?![${WORD_CHARS}])\\s*[:==:]\\s*[${CRED_VALUE_CHARS}]{4,}`, 'gi'), label: '密钥' },
|
|
141
169
|
// 本项目自有令牌(issue #45):批次71 已把形态加进**写入闸门**的禁表,但自动
|
|
142
170
|
// 记忆走的是 mdcg_remember(gated=true)、**不过 audit**,此处是这条路上唯一的
|
|
143
171
|
// 防线——此前不认自家令牌,用户粘一次即明文落进共用记忆库。
|
|
@@ -275,14 +303,17 @@ function sessionIdOf(raw) {
|
|
|
275
303
|
const UNASSIGNED_SESSION = 'unassigned';
|
|
276
304
|
/** H1 **会话级**判据:这条 session 是否「子代理/委派子会话」。
|
|
277
305
|
*
|
|
278
|
-
* 字段来源(DSH 类型面,
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
306
|
+
* 字段来源(DSH 类型面,dsh-session/lib/types/types.d.ts):实装 DSH 2.0
|
|
307
|
+
* (`dsh-0.2.0-rc.2`):81 `header.origin?: 'subagent'`「Coarse product
|
|
308
|
+
* classification for a session created as a subagent child」;:87
|
|
309
|
+
* `header.delegationDepth?: number`「absent (zero) for a top-level session,
|
|
310
|
+
* parent depth + 1 for a subagent child」(本仓 devDeps `0.1.0-rc.8` 同字段
|
|
311
|
+
* 在 :64 / :70)。
|
|
283
312
|
*
|
|
284
|
-
* ⚠️
|
|
285
|
-
*
|
|
313
|
+
* ⚠️ **观测面(2026-10-05 订正)**:本机**已装** DSH 2.0(`dsh-0.2.0-rc.2`);
|
|
314
|
+
* 19 场真实会话(`sessions/…/session.v4.jsonl.zstd`)观测到会话头
|
|
315
|
+
* `delegationDepth=0` 真实在写(19/19),`origin` 键从未出现。**仍未观测**:
|
|
316
|
+
* `origin='subagent'` / `delegationDepth>0` 的真实出现。故判据取
|
|
286
317
|
* 「**字段在场且取值匹配才拦**」的形态:header 缺失 / 非对象 / 两个字段都取不到
|
|
287
318
|
* 或不匹配 → 一律返回 false(**不拦**,安全退化为既有行为),绝不因字段缺失而
|
|
288
319
|
* 报错,也不因此改变既有写入行为。
|
|
@@ -303,18 +334,22 @@ function isSubagentSession(session) {
|
|
|
303
334
|
}
|
|
304
335
|
/** H1 **消息级**判据:这条消息是否是「另一个 agent 发给本 agent 的」(委派/中继)。
|
|
305
336
|
*
|
|
306
|
-
* 字段来源(DSH 类型面,
|
|
307
|
-
* `
|
|
308
|
-
*
|
|
337
|
+
* 字段来源(DSH 类型面,dsh-llm/lib/types/message.d.ts):实装 DSH 2.0 里 `'relay'`
|
|
338
|
+
* 在 `ContextFormed` 判别联合上(:90 `readonly form: 'relay';`;:55-56 注释原文
|
|
339
|
+
* 「A message another agent addressed to this one」),该联合由各生产者按需混入自己的
|
|
340
|
+
* source 类型——2.0 的 `MessageSourceMap` 注释明确**无共享 catch-all `plugin`
|
|
341
|
+
* 类别**,`kind` 由各生产者声明在自己的模块里(:94-100;本仓 devDeps
|
|
342
|
+
* `0.1.0-rc.8` 同段在 :52 / :86)。
|
|
309
343
|
*
|
|
310
|
-
* ⚠️
|
|
311
|
-
*
|
|
344
|
+
* ⚠️ **观测面(2026-10-05 订正)**:本机**已装** DSH 2.0;19 场真实会话中
|
|
345
|
+
* `form='relay'` 从未出现(270 条 `user/message` 实测无 relay),即**真实出现
|
|
346
|
+
* 仍未观测**。故同样取「字段在场且取值匹配才拦」;source 缺失/非对象 → false。
|
|
312
347
|
*
|
|
313
|
-
* 与既有 `kind !== 'user'`
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
348
|
+
* 与既有 `kind !== 'user'` 判据的关系:`kind` 非 `'user'` 的中继**本就被**那条拦掉
|
|
349
|
+
* (19 场实测的注入类 kind 均非 `'user'`);本判据放在它**之前**,是为了
|
|
350
|
+
* ① 不把委派判定押在 `source.kind` 单点上、② 覆盖「生产者把中继标成
|
|
351
|
+
* `kind='user'` 且带 form」这一类型面之外的形态——子会话的**首轮用户提示**就可能
|
|
352
|
+
* 是这种:它与真人输入在 `kind` 上不可分,只有会话级判据(或这里的 form)能拦。 */
|
|
318
353
|
function isRelayedMessage(source) {
|
|
319
354
|
const s = source;
|
|
320
355
|
return !!s && typeof s === 'object' && s.form === 'relay';
|
|
@@ -329,15 +364,30 @@ export function installMemoryHooks(ctx, mdcg, opts) {
|
|
|
329
364
|
return;
|
|
330
365
|
}
|
|
331
366
|
const graph = mdcg;
|
|
367
|
+
/** 落盘审计(issue #56,诊断面):三处过滤分支与写入/跳过路径各留一道**盘面**
|
|
368
|
+
* 痕迹——宿主 logger 不落盘时,单看记忆侧无法区分「被设计滤除」与
|
|
369
|
+
* 「写入失败/漏记」。审计失败静默降级(见 src/lib/hook_audit.ts),
|
|
370
|
+
* 绝不冒泡进记忆路径、不改任何写入/过滤判定。 */
|
|
371
|
+
const audit = new HookAuditRecorder(opts.auditPath);
|
|
332
372
|
/** 最近一次观测到的宿主会话标识(见 sessionIdOf;空串 = 未知/无会话)。 */
|
|
333
373
|
let lastSession = '';
|
|
334
|
-
/** 记忆沉淀(fire-and-forget)。认知图未就绪则跳过并告警(不退回 AEIS)。
|
|
335
|
-
|
|
374
|
+
/** 记忆沉淀(fire-and-forget)。认知图未就绪则跳过并告警(不退回 AEIS)。
|
|
375
|
+
* `role`=null 表示**读预热**(user-recall)——审计只统计写入路径,
|
|
376
|
+
* 故读预热不参与 written/skipped 计数(它不写记忆)。 */
|
|
377
|
+
const memorize = (label, role, run) => {
|
|
336
378
|
if (!graph.isReady()) {
|
|
379
|
+
if (role)
|
|
380
|
+
audit.skipped('not_ready', role);
|
|
337
381
|
ctx.logger.warn(`dsh-memory: 认知图未就绪,跳过自动记忆(${label})`);
|
|
338
382
|
return;
|
|
339
383
|
}
|
|
340
|
-
|
|
384
|
+
if (role)
|
|
385
|
+
audit.written(role);
|
|
386
|
+
void run(graph).catch((err) => {
|
|
387
|
+
if (role)
|
|
388
|
+
audit.skipped('failed', role);
|
|
389
|
+
ctx.logger.warn(`dsh-memory: 自动记忆 ${label} 失败: ${err.message}`);
|
|
390
|
+
});
|
|
341
391
|
};
|
|
342
392
|
// P1 完善(GPT 审查·自动记忆脱敏):写入前过滤敏感信息(默认开启)。
|
|
343
393
|
// 命中敏感模式 → 替换为 [已过滤:类别];纯凭据消息 → 跳过写入(不落库)。
|
|
@@ -405,6 +455,7 @@ export function installMemoryHooks(ctx, mdcg, opts) {
|
|
|
405
455
|
// 召回会拿子代理的 session 去读(读错会话)。字段缺失即不拦,见 isSubagentSession。
|
|
406
456
|
if (isSubagentSession(session)) {
|
|
407
457
|
ctx.logger.info('dsh-memory: 子代理会话的自动记忆被拦(H1:header.origin/delegationDepth)');
|
|
458
|
+
audit.filtered('subagent');
|
|
408
459
|
return;
|
|
409
460
|
}
|
|
410
461
|
// 会话归属(P45):记忆写入必须带会话身份,用来区分不同会话的记忆。
|
|
@@ -415,10 +466,14 @@ export function installMemoryHooks(ctx, mdcg, opts) {
|
|
|
415
466
|
lastSession = sid;
|
|
416
467
|
const sessionTag = sid ? { session: sid } : { session: UNASSIGNED_SESSION };
|
|
417
468
|
if (event.type === 'user/message' && opts.userMessage) {
|
|
469
|
+
// 落盘审计(issue #56):source.kind 分布——先于两级判据记录**完整**输入分布
|
|
470
|
+
// (含被滤与放行),「宿主到底给这条消息标了什么 kind」是排障第一问。
|
|
471
|
+
audit.observeKind(kindOf(event.data.source));
|
|
418
472
|
// H1 **消息级**判据:委派/中继消息(`form: 'relay'` 语义=「另一个 agent
|
|
419
473
|
// 发给本 agent 的消息」)不写。先于 kind 判据,理由见 isRelayedMessage 注释。
|
|
420
474
|
if (isRelayedMessage(event.data.source)) {
|
|
421
475
|
ctx.logger.info('dsh-memory: 委派/中继消息的自动记忆被拦(H1:source.form=relay)');
|
|
476
|
+
audit.filtered('relay', kindOf(event.data.source));
|
|
422
477
|
return;
|
|
423
478
|
}
|
|
424
479
|
// 只记真实用户输入(kind='user'),跳过插件注入/系统上下文
|
|
@@ -426,15 +481,18 @@ export function installMemoryHooks(ctx, mdcg, opts) {
|
|
|
426
481
|
// T4 诊断(2026-08-30):dsh 端对话零写入排查——记录被滤事件的实际
|
|
427
482
|
// source.kind(若 dsh 新版改了 kind 值,此处日志可定位)
|
|
428
483
|
ctx.logger.info(`dsh-memory: user/message 事件被滤(source.kind=${event.data.source?.kind ?? 'undefined'})`);
|
|
484
|
+
audit.filtered('kind', kindOf(event.data.source));
|
|
429
485
|
return;
|
|
430
486
|
}
|
|
431
487
|
const text = extractText(event.data.content);
|
|
432
488
|
if (!text)
|
|
433
489
|
return;
|
|
434
490
|
const safe = sanitize(text); // 脱敏:纯凭据消息 → null → 跳过写入
|
|
435
|
-
if (safe === null)
|
|
491
|
+
if (safe === null) {
|
|
492
|
+
audit.skipped('sanitized', 'user');
|
|
436
493
|
return;
|
|
437
|
-
|
|
494
|
+
}
|
|
495
|
+
memorize('user', 'user', (g) => g.remember(safe, {
|
|
438
496
|
role: 'user', tags: ['dsh', 'user'], importance: opts.importance,
|
|
439
497
|
...sessionTag,
|
|
440
498
|
}));
|
|
@@ -450,16 +508,18 @@ export function installMemoryHooks(ctx, mdcg, opts) {
|
|
|
450
508
|
// 改 mdcg_client.ts(本件放行面之外),故在调用点改走等价出口。
|
|
451
509
|
// 带上它不构成越权:cg 读路径的 session 是**归因/视图**维度,不参与任何授权
|
|
452
510
|
// (issue #35 定稿「身份不可自报」,见 md_cg/mdcos.py 的 _candidates)。
|
|
453
|
-
memorize('user-recall', (g) => g.read(safe.slice(0, 200), { k: 3, ...sessionTag }));
|
|
511
|
+
memorize('user-recall', null, (g) => g.read(safe.slice(0, 200), { k: 3, ...sessionTag }));
|
|
454
512
|
}
|
|
455
513
|
else if (event.type === 'assistant/message' && opts.assistantMessage) {
|
|
456
514
|
const text = extractText(event.data.message.content);
|
|
457
515
|
if (!text)
|
|
458
516
|
return;
|
|
459
517
|
const safe = sanitize(text);
|
|
460
|
-
if (safe === null)
|
|
518
|
+
if (safe === null) {
|
|
519
|
+
audit.skipped('sanitized', 'assistant');
|
|
461
520
|
return;
|
|
462
|
-
|
|
521
|
+
}
|
|
522
|
+
memorize('assistant', 'assistant', (g) => g.remember(safe, {
|
|
463
523
|
role: 'assistant', tags: ['dsh', 'assistant'], importance: opts.importance * 0.8,
|
|
464
524
|
...sessionTag,
|
|
465
525
|
}));
|
|
@@ -471,9 +531,11 @@ export function installMemoryHooks(ctx, mdcg, opts) {
|
|
|
471
531
|
if (!text)
|
|
472
532
|
return;
|
|
473
533
|
const safe = sanitize(text);
|
|
474
|
-
if (safe === null)
|
|
534
|
+
if (safe === null) {
|
|
535
|
+
audit.skipped('sanitized', 'tool');
|
|
475
536
|
return;
|
|
476
|
-
|
|
537
|
+
}
|
|
538
|
+
memorize('tool', 'tool', (g) => g.remember(safe, {
|
|
477
539
|
role: 'tool-output', tags: ['dsh', 'tool'], importance: opts.importance * 0.6,
|
|
478
540
|
...sessionTag,
|
|
479
541
|
}));
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** 三处过滤分支的成因(与 hooks.ts 一一对应:子代理会话 / 中继消息 / kind 非 user)。 */
|
|
2
|
+
export type HookFilterReason = 'subagent' | 'relay' | 'kind';
|
|
3
|
+
/** 写入路径的角色(与 md_cg 对 DSH 会话事件的 role 约定一致)。 */
|
|
4
|
+
export type HookWriteRole = 'user' | 'assistant' | 'tool';
|
|
5
|
+
/** 跳过写入的成因。 */
|
|
6
|
+
export type HookSkipReason = 'sanitized' | 'not_ready' | 'failed';
|
|
7
|
+
/** last 的一条摘要(**绝不含消息内容**——只有动作、成因、kind 与 role)。 */
|
|
8
|
+
export interface HookAuditEntry {
|
|
9
|
+
ts: string;
|
|
10
|
+
action: 'filtered' | 'written' | 'skipped';
|
|
11
|
+
reason?: HookFilterReason | HookSkipReason;
|
|
12
|
+
kind?: string;
|
|
13
|
+
role?: HookWriteRole;
|
|
14
|
+
}
|
|
15
|
+
/** 审计状态(= 状态文件的结构)。 */
|
|
16
|
+
export interface HookAuditState {
|
|
17
|
+
counts: {
|
|
18
|
+
/** 三处过滤分支的计数。 */
|
|
19
|
+
filtered: Record<HookFilterReason, number>;
|
|
20
|
+
/** 成功发起的写入计数(按 role 分解)。 */
|
|
21
|
+
written: Record<HookWriteRole, number>;
|
|
22
|
+
/** 跳过写入的计数(脱敏置空 / 认知图未就绪 / 写入失败)。 */
|
|
23
|
+
skipped: Record<HookSkipReason, number>;
|
|
24
|
+
};
|
|
25
|
+
/** 进入消息级处理的 `source.kind` 分布(缺字段/空串/非字符串记 "undefined")。 */
|
|
26
|
+
kinds: Record<string, number>;
|
|
27
|
+
/** 最近 ≤MAX_LAST 条摘要。 */
|
|
28
|
+
last: HookAuditEntry[];
|
|
29
|
+
updated_at: string;
|
|
30
|
+
}
|
|
31
|
+
/** 缺省审计文件:与桥探针 / apply 探针同目录同惯例(README.md:110)。 */
|
|
32
|
+
export declare const DEFAULT_HOOK_AUDIT_PATH: string;
|
|
33
|
+
/** `source.kind` 的规范化取值:非字符串(含缺字段)或空串一律记 "undefined"
|
|
34
|
+
* (不可辨认即无值——空串与缺字段在排障上同义)。 */
|
|
35
|
+
export declare function kindOf(source: unknown): string;
|
|
36
|
+
/**
|
|
37
|
+
* 有界落盘审计记录器(**静默降级**:任何公共方法都不会抛错)。
|
|
38
|
+
*
|
|
39
|
+
* 用法(hooks.ts 内):`new HookAuditRecorder(opts.auditPath)`,随后在
|
|
40
|
+
* 三处过滤分支与写入/跳过路径各调一次对应方法。审计失败只影响审计自身。
|
|
41
|
+
*/
|
|
42
|
+
export declare class HookAuditRecorder {
|
|
43
|
+
#private;
|
|
44
|
+
/** 实际生效的审计文件路径(构造时定死,供日志/测试核对)。 */
|
|
45
|
+
readonly path: string;
|
|
46
|
+
constructor(path?: string);
|
|
47
|
+
/** 一条**被设计滤除**的消息(三处过滤分支各调一次)。 */
|
|
48
|
+
filtered(reason: HookFilterReason, kind?: string): void;
|
|
49
|
+
/** `source.kind` 分布(每条**进入消息级处理**的事件调一次,含被滤与放行)。 */
|
|
50
|
+
observeKind(kind: string): void;
|
|
51
|
+
/** 成功**发起**的写入(按 role;发起后失败另记 skipped.failed)。 */
|
|
52
|
+
written(role: HookWriteRole): void;
|
|
53
|
+
/** 跳过写入(脱敏置空 / 认知图未就绪 / 写入失败)。 */
|
|
54
|
+
skipped(reason: HookSkipReason, role?: HookWriteRole): void;
|
|
55
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hook_audit.ts · 自动记忆钩子的**有界落盘审计**(issue #56,诊断面)
|
|
3
|
+
*
|
|
4
|
+
* 病灶(报告人 jiamajie,v0.7.1 实测):src/hooks.ts 的三处过滤分支(子代理会话 /
|
|
5
|
+
* 中继消息 / `source.kind !== 'user'`)只调 `ctx.logger.info`——宿主 logger 不落盘,
|
|
6
|
+
* 盘面完全无痕。其它插件按设计注入的 `role:"user"` 消息(如 @michengai/dsh-pua 的
|
|
7
|
+
* PUA_RUNTIME_V1 提示)不进记忆库时,从记忆侧只能看到「少了一条」,无法区分
|
|
8
|
+
* 「被设计滤除」与「写入失败/漏记」。
|
|
9
|
+
*
|
|
10
|
+
* 本模块 = **诊断面**,不参与任何写入/过滤判定(红线:哪些消息进记忆一字不动):
|
|
11
|
+
* ① 维护一份小 JSON 状态文件(缺省 `~/.dsh/logs/dsh-memory-hook-audit.json`,
|
|
12
|
+
* 与 `lingshu-bridge-debug.log` / `dsh-memory-apply-error.log` 同目录同惯例,
|
|
13
|
+
* 见 README.md:110);
|
|
14
|
+
* ② 每事件只写计数与「最近条目」摘要(action/reason/kind/role),**绝不记录
|
|
15
|
+
* 消息内容**(内容可能含隐私/凭据,审计文件是可被随手打开的诊断件);
|
|
16
|
+
* ③ 原子写:同目录临时文件 + rename(读者永远不会看到半截 JSON);
|
|
17
|
+
* ④ 有界:last 截断到 MAX_LAST=20(无追加式增长);kinds 键数上限 MAX_KINDS,
|
|
18
|
+
* 超限归并到 "other"(kind 值域理论上由宿主决定,防异常值把文件撑大);
|
|
19
|
+
* ⑤ 静默降级:公共方法全部 try/catch 包死——审计失败绝不冒泡进记忆路径
|
|
20
|
+
* (审计是增益,记忆写入必须照常)。
|
|
21
|
+
*
|
|
22
|
+
* 为什么落盘采用**同步** fs:`session/event` 回调是同步的,异步落盘会引入
|
|
23
|
+
* 「多事件并发写同一文件」的乱序与竞态;状态文件只有几 KB,一次同步读(仅首次)
|
|
24
|
+
* 加一次全量重写的代价远小于它守护的诊断价值。
|
|
25
|
+
*
|
|
26
|
+
* 与 ctx.logger.info 的关系:hook 内三行 info 日志**保留不动**(in-memory 实时
|
|
27
|
+
* 视图),本审计是**盘面**视图(宿主 logger 不落盘时仍可复盘)。
|
|
28
|
+
*
|
|
29
|
+
* 边界(如实):
|
|
30
|
+
* · 状态是「进程内缓存 + 每次全量重写」,多进程同时写同一路径会互相覆盖
|
|
31
|
+
* (插件单实例部署下不构成问题;审计非关键路径,不做跨进程锁);
|
|
32
|
+
* · 审计文件损坏/手改致形态不符时按空态重建(数值归零、last 逐条过滤),
|
|
33
|
+
* 不抛错、不保留非法内容;
|
|
34
|
+
* · `written` 记的是「**成功发起**」的写入(fire-and-forget 的发起时刻),
|
|
35
|
+
* 发起后失败另记 `skipped.failed`——与 hooks.ts 的 memorize 两段式对应。
|
|
36
|
+
*/
|
|
37
|
+
import { mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
38
|
+
import { homedir } from 'node:os';
|
|
39
|
+
import { dirname, join } from 'node:path';
|
|
40
|
+
/** 缺省审计文件:与桥探针 / apply 探针同目录同惯例(README.md:110)。 */
|
|
41
|
+
export const DEFAULT_HOOK_AUDIT_PATH = join(homedir(), '.dsh', 'logs', 'dsh-memory-hook-audit.json');
|
|
42
|
+
/** last 条数上限(有界性单点——没有它 audit 文件会随会话无限增长)。 */
|
|
43
|
+
const MAX_LAST = 20;
|
|
44
|
+
/** kinds 键数上限(超限归并到 OTHER_KIND,防宿主 kind 值域异常撑大文件)。 */
|
|
45
|
+
const MAX_KINDS = 32;
|
|
46
|
+
/** kinds 归并桶。 */
|
|
47
|
+
const OTHER_KIND = 'other';
|
|
48
|
+
const FILTER_REASONS = ['subagent', 'relay', 'kind'];
|
|
49
|
+
const WRITE_ROLES = ['user', 'assistant', 'tool'];
|
|
50
|
+
const SKIP_REASONS = ['sanitized', 'not_ready', 'failed'];
|
|
51
|
+
/** `source.kind` 的规范化取值:非字符串(含缺字段)或空串一律记 "undefined"
|
|
52
|
+
* (不可辨认即无值——空串与缺字段在排障上同义)。 */
|
|
53
|
+
export function kindOf(source) {
|
|
54
|
+
const k = source?.kind;
|
|
55
|
+
return typeof k === 'string' && k.length > 0 ? k : 'undefined';
|
|
56
|
+
}
|
|
57
|
+
function nowIso() {
|
|
58
|
+
return new Date().toISOString();
|
|
59
|
+
}
|
|
60
|
+
function emptyState() {
|
|
61
|
+
return {
|
|
62
|
+
counts: {
|
|
63
|
+
filtered: { subagent: 0, relay: 0, kind: 0 },
|
|
64
|
+
written: { user: 0, assistant: 0, tool: 0 },
|
|
65
|
+
skipped: { sanitized: 0, not_ready: 0, failed: 0 },
|
|
66
|
+
},
|
|
67
|
+
kinds: {},
|
|
68
|
+
last: [],
|
|
69
|
+
updated_at: nowIso(),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** 计数位:只认有限非负整数,其余归零(防手改/坏文件把计数变成 NaN)。 */
|
|
73
|
+
function countOf(v) {
|
|
74
|
+
return typeof v === 'number' && Number.isFinite(v) && v >= 0 ? Math.floor(v) : 0;
|
|
75
|
+
}
|
|
76
|
+
/** 盘面内容 → 状态:逐字段归并,形态不符的字段一律丢弃(不信任盘面内容)。 */
|
|
77
|
+
function mergeState(raw) {
|
|
78
|
+
const s = emptyState();
|
|
79
|
+
if (!raw || typeof raw !== 'object')
|
|
80
|
+
return s;
|
|
81
|
+
const r = raw;
|
|
82
|
+
if (r.counts && typeof r.counts === 'object') {
|
|
83
|
+
const c = r.counts;
|
|
84
|
+
const buckets = [
|
|
85
|
+
[s.counts.filtered, FILTER_REASONS, c.filtered],
|
|
86
|
+
[s.counts.written, WRITE_ROLES, c.written],
|
|
87
|
+
[s.counts.skipped, SKIP_REASONS, c.skipped],
|
|
88
|
+
];
|
|
89
|
+
for (const [dst, keys, src] of buckets) {
|
|
90
|
+
if (!src || typeof src !== 'object')
|
|
91
|
+
continue;
|
|
92
|
+
for (const key of keys)
|
|
93
|
+
dst[key] = countOf(src[key]);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (r.kinds && typeof r.kinds === 'object') {
|
|
97
|
+
for (const [k, v] of Object.entries(r.kinds)) {
|
|
98
|
+
if (typeof v === 'number' && Number.isFinite(v) && v > 0)
|
|
99
|
+
s.kinds[k] = Math.floor(v);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (Array.isArray(r.last)) {
|
|
103
|
+
for (const e of r.last) {
|
|
104
|
+
if (!e || typeof e !== 'object')
|
|
105
|
+
continue;
|
|
106
|
+
const entry = e;
|
|
107
|
+
if (typeof entry.ts !== 'string')
|
|
108
|
+
continue;
|
|
109
|
+
if (entry.action !== 'filtered' && entry.action !== 'written' && entry.action !== 'skipped')
|
|
110
|
+
continue;
|
|
111
|
+
s.last.push(entry);
|
|
112
|
+
}
|
|
113
|
+
if (s.last.length > MAX_LAST)
|
|
114
|
+
s.last = s.last.slice(-MAX_LAST);
|
|
115
|
+
}
|
|
116
|
+
return s;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* 有界落盘审计记录器(**静默降级**:任何公共方法都不会抛错)。
|
|
120
|
+
*
|
|
121
|
+
* 用法(hooks.ts 内):`new HookAuditRecorder(opts.auditPath)`,随后在
|
|
122
|
+
* 三处过滤分支与写入/跳过路径各调一次对应方法。审计失败只影响审计自身。
|
|
123
|
+
*/
|
|
124
|
+
export class HookAuditRecorder {
|
|
125
|
+
/** 实际生效的审计文件路径(构造时定死,供日志/测试核对)。 */
|
|
126
|
+
path;
|
|
127
|
+
/** 进程内状态缓存(首次记录时从盘面载入,此后只增量改内存 + 全量重写)。 */
|
|
128
|
+
#state = null;
|
|
129
|
+
constructor(path) {
|
|
130
|
+
this.path = typeof path === 'string' && path.trim() ? path.trim() : DEFAULT_HOOK_AUDIT_PATH;
|
|
131
|
+
}
|
|
132
|
+
/** 一条**被设计滤除**的消息(三处过滤分支各调一次)。 */
|
|
133
|
+
filtered(reason, kind) {
|
|
134
|
+
this.#mutate((s) => {
|
|
135
|
+
s.counts.filtered[reason] += 1;
|
|
136
|
+
s.last.push(kind === undefined
|
|
137
|
+
? { ts: nowIso(), action: 'filtered', reason }
|
|
138
|
+
: { ts: nowIso(), action: 'filtered', reason, kind });
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/** `source.kind` 分布(每条**进入消息级处理**的事件调一次,含被滤与放行)。 */
|
|
142
|
+
observeKind(kind) {
|
|
143
|
+
this.#mutate((s) => {
|
|
144
|
+
const key = this.#boundedKindKey(s, kind);
|
|
145
|
+
s.kinds[key] = (s.kinds[key] ?? 0) + 1;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/** 成功**发起**的写入(按 role;发起后失败另记 skipped.failed)。 */
|
|
149
|
+
written(role) {
|
|
150
|
+
this.#mutate((s) => {
|
|
151
|
+
s.counts.written[role] += 1;
|
|
152
|
+
s.last.push({ ts: nowIso(), action: 'written', role });
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/** 跳过写入(脱敏置空 / 认知图未就绪 / 写入失败)。 */
|
|
156
|
+
skipped(reason, role) {
|
|
157
|
+
this.#mutate((s) => {
|
|
158
|
+
s.counts.skipped[reason] += 1;
|
|
159
|
+
s.last.push(role === undefined
|
|
160
|
+
? { ts: nowIso(), action: 'skipped', reason }
|
|
161
|
+
: { ts: nowIso(), action: 'skipped', reason, role });
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
/** 键数有界:已见键原样用;新键在未达上限时收录,达限后统一归并到 "other"。 */
|
|
165
|
+
#boundedKindKey(s, kind) {
|
|
166
|
+
if (Object.prototype.hasOwnProperty.call(s.kinds, kind))
|
|
167
|
+
return kind;
|
|
168
|
+
return Object.keys(s.kinds).length >= MAX_KINDS ? OTHER_KIND : kind;
|
|
169
|
+
}
|
|
170
|
+
/** 读改写 + 原子落盘;整段 try/catch——审计失败静默降级(绝不冒泡进记忆路径)。 */
|
|
171
|
+
#mutate(fn) {
|
|
172
|
+
try {
|
|
173
|
+
const s = this.#state ?? (this.#state = this.#load());
|
|
174
|
+
fn(s);
|
|
175
|
+
if (s.last.length > MAX_LAST)
|
|
176
|
+
s.last = s.last.slice(-MAX_LAST);
|
|
177
|
+
s.updated_at = nowIso();
|
|
178
|
+
this.#save(s);
|
|
179
|
+
}
|
|
180
|
+
catch { /* 审计失败静默降级:记忆路径必须照常 */ }
|
|
181
|
+
}
|
|
182
|
+
/** 盘面 → 状态;不存在/不可读/坏 JSON/形态不符 → 空态重建(不抛)。 */
|
|
183
|
+
#load() {
|
|
184
|
+
try {
|
|
185
|
+
return mergeState(JSON.parse(readFileSync(this.path, 'utf8')));
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return emptyState();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/** 原子写:同目录临时文件 + rename(临时文件在 rename 失败时清理)。 */
|
|
192
|
+
#save(s) {
|
|
193
|
+
const dir = dirname(this.path);
|
|
194
|
+
mkdirSync(dir, { recursive: true });
|
|
195
|
+
const tmp = `${this.path}.${process.pid.toString(36)}.${Date.now().toString(36)}.tmp`;
|
|
196
|
+
writeFileSync(tmp, `${JSON.stringify(s, null, 2)}\n`, 'utf8');
|
|
197
|
+
try {
|
|
198
|
+
renameSync(tmp, this.path);
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
try {
|
|
202
|
+
unlinkSync(tmp);
|
|
203
|
+
}
|
|
204
|
+
catch { /* 清理失败忽略:残留 .tmp 不影响读路径 */ }
|
|
205
|
+
throw err;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
package/lib/lib/mutual.js
CHANGED
|
@@ -211,8 +211,12 @@ export async function llmReview(claim, whitebox, reviewFn) {
|
|
|
211
211
|
export function combineVerdict(w, l) {
|
|
212
212
|
const c = l.conclusion || '';
|
|
213
213
|
// P1 修复(GPT 审查):白箱通道异常 → fail-closed(不因复核「同意」而 pass)
|
|
214
|
+
// N268:**复核通道**异常的标记由 llmReview 写在 `l.conclusion`(`llm_error: …`)——
|
|
215
|
+
// 此前只查 w.judgment(该字段只可能由 whiteboxVerify 写 whitebox_error)⇒ 该分支
|
|
216
|
+
// 是死条件,复核抛异常时白箱「采纳」径直判 pass(复核不可用反成「通过」)。
|
|
217
|
+
// 两字段都查:l 侧是真正的生产者口径,w 侧保留同形防御(防上游把异常塞进白箱字段)。
|
|
214
218
|
if (w.judgment.startsWith('whitebox_error') || w.judgment.startsWith('llm_error')
|
|
215
|
-
|| w.d_norm < 0) {
|
|
219
|
+
|| c.startsWith('llm_error') || w.d_norm < 0) {
|
|
216
220
|
return 'needs_revision';
|
|
217
221
|
}
|
|
218
222
|
if (w.judgment.startsWith('采纳')) {
|
package/lib/lib/roleplay_web.js
CHANGED
|
@@ -844,6 +844,17 @@ export async function installRoleplayWeb(ctx, capability, config, disposers, mdc
|
|
|
844
844
|
res.end(JSON.stringify({ ok: false, error: '非法角色 id(白名单 ^[A-Za-z0-9_.-]{1,64}$)' }));
|
|
845
845
|
return;
|
|
846
846
|
}
|
|
847
|
+
// N266:入口类型闸——message 必须是字符串。此前这段直接
|
|
848
|
+
// `p.message.includes(w)`:message 为数组时静默走
|
|
849
|
+
// Array.prototype.includes(逐元素**全等**比较),把敏感词拆进
|
|
850
|
+
// 不同元素即整条绕过硬拦截;数字/对象/缺省则抛 TypeError 落 500
|
|
851
|
+
// (内部异常文本随响应外泄)。与 role_id 同口径:非法形态在入口
|
|
852
|
+
// 一律 400 拒——不转发引擎、不落转录。
|
|
853
|
+
if (typeof p.message !== 'string') {
|
|
854
|
+
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
855
|
+
res.end(JSON.stringify({ ok: false, route: 'refused', refused: 'bad_message', error: 'message 必须是字符串(非字符串形态不得绕过内容硬拦截,N266)' }));
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
847
858
|
// P1 完善(会话隔离):按客户端实例隔离 session 与转录
|
|
848
859
|
const cid = clientIdOf(req);
|
|
849
860
|
// —— 服务端内容分级硬拦截(不依赖前端,法律与协议保护)——
|
|
@@ -857,13 +868,17 @@ export async function installRoleplayWeb(ctx, capability, config, disposers, mdc
|
|
|
857
868
|
res.end(JSON.stringify({ reply: '⛔ 已拒绝:涉及未成年人的性内容违反国家法律与灵枢协议(未成年人保护)。灵枢不提供任何涉及未成年人的性扮演内容。', route: 'refused', refused: 'minor_nsfw' }));
|
|
858
869
|
return;
|
|
859
870
|
}
|
|
860
|
-
appendTranscript(role, { time: Date.now(), role: 'user', text: p.message }, cid);
|
|
861
|
-
void toGraph('user-turn', (g) => g.writeTranscript(role, cid, 'user', p.message));
|
|
862
871
|
if (!capReady()) {
|
|
863
872
|
res.writeHead(503, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
864
873
|
res.end(JSON.stringify({ reply: '', route: 'unavailable', error: CAP_ABSENT_MSG }));
|
|
865
874
|
return;
|
|
866
875
|
}
|
|
876
|
+
// N266:落盘移到内容门控与就绪闸**之后**——被内容门控拒绝的、以及
|
|
877
|
+
// 「能力未接入」(503)的回合都从未发生,此前这两笔写在校验与
|
|
878
|
+
// capReady 之前,未经门控的 message 原文会被留进本地转录
|
|
879
|
+
//(读同一文件的历史接口随即原样回放)。
|
|
880
|
+
appendTranscript(role, { time: Date.now(), role: 'user', text: p.message }, cid);
|
|
881
|
+
void toGraph('user-turn', (g) => g.writeTranscript(role, cid, 'user', p.message));
|
|
867
882
|
const r = await capability.callTool('roleplay_chat', {
|
|
868
883
|
message: p.message, role_id: role, session_id: sessionFor(role, cid), data_dir: roleDataDir,
|
|
869
884
|
});
|