@furongjun1999/dsh-memory 0.7.2 → 0.7.4
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 +57 -47
- package/codebuddy/CODEBUDDY.md +6 -6
- 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/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/217/221/345/270/20312_/344/270/226/347/225/214/346/250/241/345/236/213/345/212/237/350/203/275/347/253/257_v1.0.md +108 -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/eval//350/257/255/344/271/211/346/227/266/347/251/272/345/233/276/350/241/245/345/205/250_P1_/346/212/275/345/217/226/345/231/250v4_/344/270/211/347/216/207/350/257/273/346/225/260_v1.0.md +231 -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//344/270/226/347/225/214/346/250/241/345/236/213/345/212/237/350/203/275/347/253/257_/344/275/277/347/224/250/344/270/216/350/277/220/347/273/264_v1.0.md +305 -0
- 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 +45 -42
- 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 +6 -5
- 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 +134 -50
- 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/lib/lib/session_state.d.ts +56 -0
- package/lib/lib/session_state.js +124 -0
- package/lib/tools.js +12 -1
- package/md_cg/auditview.py +248 -0
- package/md_cg/bench_e2e_judge.py +4 -2
- package/md_cg/branches.py +29 -3
- 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/lifecycle.py +38 -0
- package/md_cg/mcp_server.py +122 -12
- package/md_cg/mdcg.py +4923 -4822
- package/md_cg/mdcos.py +109 -21
- package/md_cg/nodefile.py +77 -13
- package/md_cg/readcache.py +29 -4
- 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/state_slots.py +221 -0
- package/md_cg/stg.py +68 -3
- 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_datapath_device_name.py +62 -3
- package/md_cg/test_generation_guard.py +5 -2
- package/md_cg/test_ghostref.py +182 -0
- package/md_cg/test_interop.py +1 -1
- package/md_cg/test_issue52_scan_condition_first.py +1 -1
- package/md_cg/test_lifecycle_retire_leak.py +408 -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_read_face_semantics.py +6 -3
- 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_event_op.py +514 -0
- package/md_cg/test_state_events.py +178 -0
- package/md_cg/test_state_slots.py +691 -0
- package/md_cg/test_time_core_lint.py +8 -1
- package/md_cg/test_writepipe.py +8 -5
- package/md_cg/tokens.py +18 -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 +599 -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/src/lib/session_state.ts +127 -0
- package/src/tools.ts +12 -1
- package/zcode/AGENTS.md +6 -6
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,10 @@
|
|
|
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';
|
|
49
|
+
// 运行期会话状态单点(B 治本批):观测在 hooks 面,写归因注入在工具面
|
|
50
|
+
// (src/tools.ts)——两处共用同一状态,见 lib/session_state.ts 头注。
|
|
51
|
+
import { currentSession, noteSession, UNASSIGNED_SESSION } from './lib/session_state.js';
|
|
46
52
|
/** 从 ContentBlock[] 提取纯文本。 */
|
|
47
53
|
function extractText(blocks) {
|
|
48
54
|
const parts = [];
|
|
@@ -88,8 +94,18 @@ export const WORD_CHARS = `A-Za-z0-9_${FW_DIGITS}${FW_UPPER}${FW_LOWER}_`;
|
|
|
88
94
|
/** 两宽数字类。 */
|
|
89
95
|
const DIGIT_CHARS = `0-9${FW_DIGITS}`;
|
|
90
96
|
/** 两宽凭据值类:词字符 + `@ # $ % ^ & * ! . -` 的两宽形态。
|
|
91
|
-
* ASCII 连字符一律转义(`\\-`)——`[_--]` 会被解析成 `_`→`-` 的**巨区间**。
|
|
92
|
-
|
|
97
|
+
* ASCII 连字符一律转义(`\\-`)——`[_--]` 会被解析成 `_`→`-` 的**巨区间**。
|
|
98
|
+
*
|
|
99
|
+
* N265(本轮补齐):半角 `&` 与全角 `#%^*` 此前缺位——其对照形态(全角 `&`、
|
|
100
|
+
* 半角 `# % ^ *`)早在类里,构成「同形不同过滤」:缺口字符落在值前 4 位内 ⇒ 整条漏检、
|
|
101
|
+
* 落在第 4 位之后 ⇒ 命中被截断在缺口处、尾部留明文。守卫
|
|
102
|
+
* test/fullwidth_redact.test.ts ⑧(形态一)与 ⑨(形态二)钉住这两只退化形态。
|
|
103
|
+
*
|
|
104
|
+
* ⚠️ 全角 `!` **不在**本类,且与半角 `!` 不对称(`!` 在类)——这是**有意保留**的现口径:
|
|
105
|
+
* 本文件「边界」段与同文件守卫 ④ 把全角叹号钉为句读边界(值在此收住)。
|
|
106
|
+
* 2026-10-05 使用者裁决:判 by-design——保留全角 ! 为句读边界,不得并入值类
|
|
107
|
+
* (并入必动守卫④),**勿在补字符集时顺手塞进来**——否则守卫 ④ 会红。 */
|
|
108
|
+
const CRED_VALUE_CHARS = `${WORD_CHARS}@@##$$%%^^&&**!.\\--.`;
|
|
93
109
|
/** 两宽令牌值类(本项目令牌 id/secret 的字符集:词字符 + `-`)。
|
|
94
110
|
* 导出供守卫核对字面量规则的同源性(同 ⑦),不承诺稳定 ABI。 */
|
|
95
111
|
export const TOKEN_VALUE_CHARS = `${WORD_CHARS}\\--`;
|
|
@@ -132,12 +148,27 @@ export const SENSITIVE_PATTERNS = [
|
|
|
132
148
|
+ `(?:${twoWidth('password')}|${twoWidth('passwd')}|${twoWidth('pwd')})`
|
|
133
149
|
+ `(?![${WORD_CHARS}])\\s*[:==:]\\s*[^\\s,,。;;]+`, 'gi'), label: '密码' },
|
|
134
150
|
{ re: new RegExp(`${twoWidth('Bearer')}\\s+[${BEARER_VALUE_CHARS}]{8,}`, 'gi'), label: '令牌' },
|
|
135
|
-
//
|
|
151
|
+
// 中文/日文密码标签:值限定非中文连续串(凭据特征),避免误伤「密码是重要的安全概念」;
|
|
136
152
|
// 分隔符补全角等号 `=`(半角 `=` 本就不在本规则的集合里,故只补全角形态)。
|
|
137
|
-
|
|
153
|
+
// N269:词形补齐——繁体「密碼」、日文「パスワード」、中文「口令」与「密码」同表同口径
|
|
154
|
+
//(三者此前整条漏检:界面承诺 :66「密码…默认过滤」,却认不出这三种常见写法);
|
|
155
|
+
// 日文 `は` 一类助词**刻意不并入分隔符集合**(`パスワードは重要です` 是普通句子,
|
|
156
|
+
// 并进去即刻误伤,与「值类不含中文」同一取舍)。
|
|
157
|
+
{ re: new RegExp(`(?:密码|密碼|口令|パスワード)\\s*[::是=]\\s*[${CRED_VALUE_CHARS}]{4,}`, 'g'), label: '密码' },
|
|
138
158
|
// 两宽:全角数字形态同样要被认(`\b` 换成两宽 lookaround,见上)
|
|
139
159
|
{ re: new RegExp(`(?<![${WORD_CHARS}])[${DIGIT_CHARS}]{17}[${DIGIT_CHARS}XxXx](?![${WORD_CHARS}])`, 'g'), label: '身份证号' },
|
|
140
160
|
{ re: new RegExp(`(?<![${WORD_CHARS}])[11][3-93-9][${DIGIT_CHARS}]{9}(?![${WORD_CHARS}])`, 'g'), label: '手机号' },
|
|
161
|
+
// N269:凭据标签词形的「标签: 值」形态——密钥 / token / secret / creds 此前整条漏检
|
|
162
|
+
//(界面承诺 :66「密钥/密码/令牌…默认过滤」,而裸 `token:`/`secret=`/`creds=` 一个都不认)。
|
|
163
|
+
// 值类取共享单点 CRED_VALUE_CHARS 并设 `{4,}` 下限(与中文密码规则同口径):比
|
|
164
|
+
// 「任意非空白续写」窄,避免吃掉 `token: 这句是说明文字` 一类普通文本;词形用
|
|
165
|
+
// twoWidth 单点生成(半角/全角拼写都认,`TOKEN:…` 同样命中);标签前后沿用
|
|
166
|
+
// 两宽 lookaround(`mytoken=`/`tokenize:` 不命中)。
|
|
167
|
+
// 位置约束:两条令牌规则必须留在数组**最后两位**(test/token_redact_parity.test.ts
|
|
168
|
+
// 的交换序按 n-2/n-1 取它们做对拍),故本规则插在其前。
|
|
169
|
+
{ re: new RegExp(`(?<![${WORD_CHARS}])`
|
|
170
|
+
+ `(?:密钥|${twoWidth('token')}|${twoWidth('secret')}|${twoWidth('creds')})`
|
|
171
|
+
+ `(?![${WORD_CHARS}])\\s*[:==:]\\s*[${CRED_VALUE_CHARS}]{4,}`, 'gi'), label: '密钥' },
|
|
141
172
|
// 本项目自有令牌(issue #45):批次71 已把形态加进**写入闸门**的禁表,但自动
|
|
142
173
|
// 记忆走的是 mdcg_remember(gated=true)、**不过 audit**,此处是这条路上唯一的
|
|
143
174
|
// 防线——此前不认自家令牌,用户粘一次即明文落进共用记忆库。
|
|
@@ -263,26 +294,21 @@ function sessionIdOf(raw) {
|
|
|
263
294
|
const v = s?.id ?? s?.sessionId;
|
|
264
295
|
return typeof v === 'string' ? v.trim() : '';
|
|
265
296
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
* ⚠️ 不可退回「不传 session 键」:md_cg 的 `Principal.__init__` 在 session 为假值时
|
|
269
|
-
* 生成**进程级随机** `sess_<hex>`(md_cg/security.py:117)——插件不传,等于让一个
|
|
270
|
-
* 进程内所有「宿主未给标识」的会话共用一个**不可辨认**的随机桶:归属在审计上既
|
|
271
|
-
* 读不出是谁、跨进程也对不上,是静默的归属丢失。
|
|
272
|
-
* 本常量把这一态写成**显式值**:跨进程一致、可辨认、可审计,且不是伪造的宿主
|
|
273
|
-
* 会话 id(非 DSH 形态,服务端 `_normalize_session` 原样采用、不会被改写成别的桶)。
|
|
274
|
-
* 要读这个桶:`stg(op=timeline, session="unassigned")`。 */
|
|
275
|
-
const UNASSIGNED_SESSION = 'unassigned';
|
|
297
|
+
// UNASSIGNED_SESSION(会话归属未知时的显式占位常量)单点已移至
|
|
298
|
+
// lib/session_state.ts(B 治本批:本文件与工具面共用同一常量与同一会话状态)。
|
|
276
299
|
/** H1 **会话级**判据:这条 session 是否「子代理/委派子会话」。
|
|
277
300
|
*
|
|
278
|
-
* 字段来源(DSH 类型面,
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
301
|
+
* 字段来源(DSH 类型面,dsh-session/lib/types/types.d.ts):实装 DSH 2.0
|
|
302
|
+
* (`dsh-0.2.0-rc.2`):81 `header.origin?: 'subagent'`「Coarse product
|
|
303
|
+
* classification for a session created as a subagent child」;:87
|
|
304
|
+
* `header.delegationDepth?: number`「absent (zero) for a top-level session,
|
|
305
|
+
* parent depth + 1 for a subagent child」(本仓 devDeps `0.1.0-rc.8` 同字段
|
|
306
|
+
* 在 :64 / :70)。
|
|
283
307
|
*
|
|
284
|
-
* ⚠️
|
|
285
|
-
*
|
|
308
|
+
* ⚠️ **观测面(2026-10-05 订正)**:本机**已装** DSH 2.0(`dsh-0.2.0-rc.2`);
|
|
309
|
+
* 19 场真实会话(`sessions/…/session.v4.jsonl.zstd`)观测到会话头
|
|
310
|
+
* `delegationDepth=0` 真实在写(19/19),`origin` 键从未出现。**仍未观测**:
|
|
311
|
+
* `origin='subagent'` / `delegationDepth>0` 的真实出现。故判据取
|
|
286
312
|
* 「**字段在场且取值匹配才拦**」的形态:header 缺失 / 非对象 / 两个字段都取不到
|
|
287
313
|
* 或不匹配 → 一律返回 false(**不拦**,安全退化为既有行为),绝不因字段缺失而
|
|
288
314
|
* 报错,也不因此改变既有写入行为。
|
|
@@ -303,18 +329,22 @@ function isSubagentSession(session) {
|
|
|
303
329
|
}
|
|
304
330
|
/** H1 **消息级**判据:这条消息是否是「另一个 agent 发给本 agent 的」(委派/中继)。
|
|
305
331
|
*
|
|
306
|
-
* 字段来源(DSH 类型面,
|
|
307
|
-
* `
|
|
308
|
-
*
|
|
332
|
+
* 字段来源(DSH 类型面,dsh-llm/lib/types/message.d.ts):实装 DSH 2.0 里 `'relay'`
|
|
333
|
+
* 在 `ContextFormed` 判别联合上(:90 `readonly form: 'relay';`;:55-56 注释原文
|
|
334
|
+
* 「A message another agent addressed to this one」),该联合由各生产者按需混入自己的
|
|
335
|
+
* source 类型——2.0 的 `MessageSourceMap` 注释明确**无共享 catch-all `plugin`
|
|
336
|
+
* 类别**,`kind` 由各生产者声明在自己的模块里(:94-100;本仓 devDeps
|
|
337
|
+
* `0.1.0-rc.8` 同段在 :52 / :86)。
|
|
309
338
|
*
|
|
310
|
-
* ⚠️
|
|
311
|
-
*
|
|
339
|
+
* ⚠️ **观测面(2026-10-05 订正)**:本机**已装** DSH 2.0;19 场真实会话中
|
|
340
|
+
* `form='relay'` 从未出现(270 条 `user/message` 实测无 relay),即**真实出现
|
|
341
|
+
* 仍未观测**。故同样取「字段在场且取值匹配才拦」;source 缺失/非对象 → false。
|
|
312
342
|
*
|
|
313
|
-
* 与既有 `kind !== 'user'`
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
343
|
+
* 与既有 `kind !== 'user'` 判据的关系:`kind` 非 `'user'` 的中继**本就被**那条拦掉
|
|
344
|
+
* (19 场实测的注入类 kind 均非 `'user'`);本判据放在它**之前**,是为了
|
|
345
|
+
* ① 不把委派判定押在 `source.kind` 单点上、② 覆盖「生产者把中继标成
|
|
346
|
+
* `kind='user'` 且带 form」这一类型面之外的形态——子会话的**首轮用户提示**就可能
|
|
347
|
+
* 是这种:它与真人输入在 `kind` 上不可分,只有会话级判据(或这里的 form)能拦。 */
|
|
318
348
|
function isRelayedMessage(source) {
|
|
319
349
|
const s = source;
|
|
320
350
|
return !!s && typeof s === 'object' && s.form === 'relay';
|
|
@@ -329,15 +359,46 @@ export function installMemoryHooks(ctx, mdcg, opts) {
|
|
|
329
359
|
return;
|
|
330
360
|
}
|
|
331
361
|
const graph = mdcg;
|
|
332
|
-
/**
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
362
|
+
/** 落盘审计(issue #56,诊断面):三处过滤分支与写入/跳过路径各留一道**盘面**
|
|
363
|
+
* 痕迹——宿主 logger 不落盘时,单看记忆侧无法区分「被设计滤除」与
|
|
364
|
+
* 「写入失败/漏记」。审计失败静默降级(见 src/lib/hook_audit.ts),
|
|
365
|
+
* 绝不冒泡进记忆路径、不改任何写入/过滤判定。 */
|
|
366
|
+
const audit = new HookAuditRecorder(opts.auditPath);
|
|
367
|
+
/** 本实例是否曾观测到会话(B 治本批)。
|
|
368
|
+
*
|
|
369
|
+
* 会话状态本体是**进程级单点**(lib/session_state.ts;hooks 面观测、工具面
|
|
370
|
+
* src/tools.ts 的写归因注入共用);而「本实例有没有观测过」是**实例级**事实:
|
|
371
|
+
* 多实例并存时(测试;或宿主重装钩子),不得让**别的实例**的观测替本实例决定
|
|
372
|
+
* 召回过滤——否则新装的钩子在尚未观测到会话时就按别的实例的会话去读
|
|
373
|
+
* (读错会话,正是 P45 会话隔离要防的形态)。真机单实例下两者等价:首个
|
|
374
|
+
* session/event 之前模块状态为空,之后回落值恒为同一单点值。
|
|
375
|
+
*
|
|
376
|
+
* ⚠️ 这是**经 owner 裁定的契约字面偏离(dwfq-7b3a555e-1)**:契约给的形态是
|
|
377
|
+
* 下方回落处直接 `|| currentSession()`(无本门);但字面形态与硬边界
|
|
378
|
+
* 「test/session-attribution.test.ts 逐字未动且全绿」互斥——该守卫 ② 以
|
|
379
|
+
* 「新建 harness = 未观测」为前提,字面回落会读成前一实例的 sess_B。裁定
|
|
380
|
+
* 接受本门,两条理由:① 保留原闭包变量「新实例 = 干净状态」的**有意**语义
|
|
381
|
+
* (新装钩子未观测时不加召回过滤);② 冻结守卫零误伤。真机单实例与字面
|
|
382
|
+
* **逐位等价**(差异窗口「本实例未观测 ∧ 模块单点非空」单实例下不可达;
|
|
383
|
+
* HMR 重载时新实例回落 '' 属更保守行为)。 */
|
|
384
|
+
let observedSession = false;
|
|
385
|
+
/** 记忆沉淀(fire-and-forget)。认知图未就绪则跳过并告警(不退回 AEIS)。
|
|
386
|
+
* `role`=null 表示**读预热**(user-recall)——审计只统计写入路径,
|
|
387
|
+
* 故读预热不参与 written/skipped 计数(它不写记忆)。 */
|
|
388
|
+
const memorize = (label, role, run) => {
|
|
336
389
|
if (!graph.isReady()) {
|
|
390
|
+
if (role)
|
|
391
|
+
audit.skipped('not_ready', role);
|
|
337
392
|
ctx.logger.warn(`dsh-memory: 认知图未就绪,跳过自动记忆(${label})`);
|
|
338
393
|
return;
|
|
339
394
|
}
|
|
340
|
-
|
|
395
|
+
if (role)
|
|
396
|
+
audit.written(role);
|
|
397
|
+
void run(graph).catch((err) => {
|
|
398
|
+
if (role)
|
|
399
|
+
audit.skipped('failed', role);
|
|
400
|
+
ctx.logger.warn(`dsh-memory: 自动记忆 ${label} 失败: ${err.message}`);
|
|
401
|
+
});
|
|
341
402
|
};
|
|
342
403
|
// P1 完善(GPT 审查·自动记忆脱敏):写入前过滤敏感信息(默认开启)。
|
|
343
404
|
// 命中敏感模式 → 替换为 [已过滤:类别];纯凭据消息 → 跳过写入(不落库)。
|
|
@@ -376,7 +437,13 @@ export function installMemoryHooks(ctx, mdcg, opts) {
|
|
|
376
437
|
// 想读**所有**会话做了什么:别走自动召回(它会串台),显式调
|
|
377
438
|
// `stg(op=timeline, session="*")`,返回项带 session 归属。
|
|
378
439
|
const hostCtx = _ctx;
|
|
379
|
-
|
|
440
|
+
// 回落 = **本实例观测门 × 模块单点值**(裁定项 dwfq-7b3a555e-1,见上方
|
|
441
|
+
// observedSession 注释):本实例尚未观测 → 回落 ''(保持「新实例 =
|
|
442
|
+
// 干净状态」,与 test/session-attribution.test.ts ② 的「未观测 → 不加
|
|
443
|
+
// 过滤」相容);已观测 → 取 lib/session_state.ts 的进程级单点值。
|
|
444
|
+
// 真机单实例下两者逐位等价(差异窗口不可达)。
|
|
445
|
+
const sid = sessionIdOf(hostCtx?.agent?.session)
|
|
446
|
+
|| (observedSession ? currentSession() : '');
|
|
380
447
|
const text = formatTimelineDecayed(await graph.timeline(recallLimit, sid ? { session: sid } : {}));
|
|
381
448
|
if (text) {
|
|
382
449
|
// 注入边界转义(issue #16):宿主 system-prompt 对 context 文本做严格
|
|
@@ -401,24 +468,34 @@ export function installMemoryHooks(ctx, mdcg, opts) {
|
|
|
401
468
|
}
|
|
402
469
|
ctx.on('session/event', (session, event) => {
|
|
403
470
|
// H1(2026-09-30)**会话级**判据:子代理/委派子会话的自动记忆**整条会话**拦掉。
|
|
404
|
-
// 位置在取 sid
|
|
405
|
-
//
|
|
471
|
+
// 位置在取 sid **之前**——子代理会话不得污染会话状态(lib/session_state.ts
|
|
472
|
+
// 单点),否则顶层会话的自动召回会拿子代理的 session 去读(读错会话)。
|
|
473
|
+
// 字段缺失即不拦,见 isSubagentSession。
|
|
406
474
|
if (isSubagentSession(session)) {
|
|
407
475
|
ctx.logger.info('dsh-memory: 子代理会话的自动记忆被拦(H1:header.origin/delegationDepth)');
|
|
476
|
+
audit.filtered('subagent');
|
|
408
477
|
return;
|
|
409
478
|
}
|
|
410
479
|
// 会话归属(P45):记忆写入必须带会话身份,用来区分不同会话的记忆。
|
|
411
480
|
// 空串 = 宿主未给出会话标识 → **显式标注 unassigned**(H2③:不落内核的进程级
|
|
412
|
-
// 随机 sess_*,也不编造宿主会话 id——见
|
|
481
|
+
// 随机 sess_*,也不编造宿主会话 id——见 lib/session_state.ts 的常量注释)。
|
|
413
482
|
const sid = sessionIdOf(session);
|
|
414
|
-
|
|
415
|
-
|
|
483
|
+
// 观测即记录(B 治本批):注入单点在 lib/session_state.ts——工具面
|
|
484
|
+
// (src/tools.ts 的写归因转发)读同一个状态;位置不动(H1 子代理闸之后)。
|
|
485
|
+
if (sid) {
|
|
486
|
+
noteSession(sid);
|
|
487
|
+
observedSession = true;
|
|
488
|
+
}
|
|
416
489
|
const sessionTag = sid ? { session: sid } : { session: UNASSIGNED_SESSION };
|
|
417
490
|
if (event.type === 'user/message' && opts.userMessage) {
|
|
491
|
+
// 落盘审计(issue #56):source.kind 分布——先于两级判据记录**完整**输入分布
|
|
492
|
+
// (含被滤与放行),「宿主到底给这条消息标了什么 kind」是排障第一问。
|
|
493
|
+
audit.observeKind(kindOf(event.data.source));
|
|
418
494
|
// H1 **消息级**判据:委派/中继消息(`form: 'relay'` 语义=「另一个 agent
|
|
419
495
|
// 发给本 agent 的消息」)不写。先于 kind 判据,理由见 isRelayedMessage 注释。
|
|
420
496
|
if (isRelayedMessage(event.data.source)) {
|
|
421
497
|
ctx.logger.info('dsh-memory: 委派/中继消息的自动记忆被拦(H1:source.form=relay)');
|
|
498
|
+
audit.filtered('relay', kindOf(event.data.source));
|
|
422
499
|
return;
|
|
423
500
|
}
|
|
424
501
|
// 只记真实用户输入(kind='user'),跳过插件注入/系统上下文
|
|
@@ -426,15 +503,18 @@ export function installMemoryHooks(ctx, mdcg, opts) {
|
|
|
426
503
|
// T4 诊断(2026-08-30):dsh 端对话零写入排查——记录被滤事件的实际
|
|
427
504
|
// source.kind(若 dsh 新版改了 kind 值,此处日志可定位)
|
|
428
505
|
ctx.logger.info(`dsh-memory: user/message 事件被滤(source.kind=${event.data.source?.kind ?? 'undefined'})`);
|
|
506
|
+
audit.filtered('kind', kindOf(event.data.source));
|
|
429
507
|
return;
|
|
430
508
|
}
|
|
431
509
|
const text = extractText(event.data.content);
|
|
432
510
|
if (!text)
|
|
433
511
|
return;
|
|
434
512
|
const safe = sanitize(text); // 脱敏:纯凭据消息 → null → 跳过写入
|
|
435
|
-
if (safe === null)
|
|
513
|
+
if (safe === null) {
|
|
514
|
+
audit.skipped('sanitized', 'user');
|
|
436
515
|
return;
|
|
437
|
-
|
|
516
|
+
}
|
|
517
|
+
memorize('user', 'user', (g) => g.remember(safe, {
|
|
438
518
|
role: 'user', tags: ['dsh', 'user'], importance: opts.importance,
|
|
439
519
|
...sessionTag,
|
|
440
520
|
}));
|
|
@@ -450,16 +530,18 @@ export function installMemoryHooks(ctx, mdcg, opts) {
|
|
|
450
530
|
// 改 mdcg_client.ts(本件放行面之外),故在调用点改走等价出口。
|
|
451
531
|
// 带上它不构成越权:cg 读路径的 session 是**归因/视图**维度,不参与任何授权
|
|
452
532
|
// (issue #35 定稿「身份不可自报」,见 md_cg/mdcos.py 的 _candidates)。
|
|
453
|
-
memorize('user-recall', (g) => g.read(safe.slice(0, 200), { k: 3, ...sessionTag }));
|
|
533
|
+
memorize('user-recall', null, (g) => g.read(safe.slice(0, 200), { k: 3, ...sessionTag }));
|
|
454
534
|
}
|
|
455
535
|
else if (event.type === 'assistant/message' && opts.assistantMessage) {
|
|
456
536
|
const text = extractText(event.data.message.content);
|
|
457
537
|
if (!text)
|
|
458
538
|
return;
|
|
459
539
|
const safe = sanitize(text);
|
|
460
|
-
if (safe === null)
|
|
540
|
+
if (safe === null) {
|
|
541
|
+
audit.skipped('sanitized', 'assistant');
|
|
461
542
|
return;
|
|
462
|
-
|
|
543
|
+
}
|
|
544
|
+
memorize('assistant', 'assistant', (g) => g.remember(safe, {
|
|
463
545
|
role: 'assistant', tags: ['dsh', 'assistant'], importance: opts.importance * 0.8,
|
|
464
546
|
...sessionTag,
|
|
465
547
|
}));
|
|
@@ -471,9 +553,11 @@ export function installMemoryHooks(ctx, mdcg, opts) {
|
|
|
471
553
|
if (!text)
|
|
472
554
|
return;
|
|
473
555
|
const safe = sanitize(text);
|
|
474
|
-
if (safe === null)
|
|
556
|
+
if (safe === null) {
|
|
557
|
+
audit.skipped('sanitized', 'tool');
|
|
475
558
|
return;
|
|
476
|
-
|
|
559
|
+
}
|
|
560
|
+
memorize('tool', 'tool', (g) => g.remember(safe, {
|
|
477
561
|
role: 'tool-output', tags: ['dsh', 'tool'], importance: opts.importance * 0.6,
|
|
478
562
|
...sessionTag,
|
|
479
563
|
}));
|
|
@@ -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
|
});
|