@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
|
@@ -0,0 +1,243 @@
|
|
|
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
|
+
|
|
41
|
+
/** 三处过滤分支的成因(与 hooks.ts 一一对应:子代理会话 / 中继消息 / kind 非 user)。 */
|
|
42
|
+
export type HookFilterReason = 'subagent' | 'relay' | 'kind'
|
|
43
|
+
/** 写入路径的角色(与 md_cg 对 DSH 会话事件的 role 约定一致)。 */
|
|
44
|
+
export type HookWriteRole = 'user' | 'assistant' | 'tool'
|
|
45
|
+
/** 跳过写入的成因。 */
|
|
46
|
+
export type HookSkipReason = 'sanitized' | 'not_ready' | 'failed'
|
|
47
|
+
|
|
48
|
+
/** last 的一条摘要(**绝不含消息内容**——只有动作、成因、kind 与 role)。 */
|
|
49
|
+
export interface HookAuditEntry {
|
|
50
|
+
ts: string
|
|
51
|
+
action: 'filtered' | 'written' | 'skipped'
|
|
52
|
+
reason?: HookFilterReason | HookSkipReason
|
|
53
|
+
kind?: string
|
|
54
|
+
role?: HookWriteRole
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 审计状态(= 状态文件的结构)。 */
|
|
58
|
+
export interface HookAuditState {
|
|
59
|
+
counts: {
|
|
60
|
+
/** 三处过滤分支的计数。 */
|
|
61
|
+
filtered: Record<HookFilterReason, number>
|
|
62
|
+
/** 成功发起的写入计数(按 role 分解)。 */
|
|
63
|
+
written: Record<HookWriteRole, number>
|
|
64
|
+
/** 跳过写入的计数(脱敏置空 / 认知图未就绪 / 写入失败)。 */
|
|
65
|
+
skipped: Record<HookSkipReason, number>
|
|
66
|
+
}
|
|
67
|
+
/** 进入消息级处理的 `source.kind` 分布(缺字段/空串/非字符串记 "undefined")。 */
|
|
68
|
+
kinds: Record<string, number>
|
|
69
|
+
/** 最近 ≤MAX_LAST 条摘要。 */
|
|
70
|
+
last: HookAuditEntry[]
|
|
71
|
+
updated_at: string
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 缺省审计文件:与桥探针 / apply 探针同目录同惯例(README.md:110)。 */
|
|
75
|
+
export const DEFAULT_HOOK_AUDIT_PATH = join(homedir(), '.dsh', 'logs', 'dsh-memory-hook-audit.json')
|
|
76
|
+
|
|
77
|
+
/** last 条数上限(有界性单点——没有它 audit 文件会随会话无限增长)。 */
|
|
78
|
+
const MAX_LAST = 20
|
|
79
|
+
/** kinds 键数上限(超限归并到 OTHER_KIND,防宿主 kind 值域异常撑大文件)。 */
|
|
80
|
+
const MAX_KINDS = 32
|
|
81
|
+
/** kinds 归并桶。 */
|
|
82
|
+
const OTHER_KIND = 'other'
|
|
83
|
+
|
|
84
|
+
const FILTER_REASONS: HookFilterReason[] = ['subagent', 'relay', 'kind']
|
|
85
|
+
const WRITE_ROLES: HookWriteRole[] = ['user', 'assistant', 'tool']
|
|
86
|
+
const SKIP_REASONS: HookSkipReason[] = ['sanitized', 'not_ready', 'failed']
|
|
87
|
+
|
|
88
|
+
/** `source.kind` 的规范化取值:非字符串(含缺字段)或空串一律记 "undefined"
|
|
89
|
+
* (不可辨认即无值——空串与缺字段在排障上同义)。 */
|
|
90
|
+
export function kindOf(source: unknown): string {
|
|
91
|
+
const k = (source as { kind?: unknown } | null | undefined)?.kind
|
|
92
|
+
return typeof k === 'string' && k.length > 0 ? k : 'undefined'
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function nowIso(): string {
|
|
96
|
+
return new Date().toISOString()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function emptyState(): HookAuditState {
|
|
100
|
+
return {
|
|
101
|
+
counts: {
|
|
102
|
+
filtered: { subagent: 0, relay: 0, kind: 0 },
|
|
103
|
+
written: { user: 0, assistant: 0, tool: 0 },
|
|
104
|
+
skipped: { sanitized: 0, not_ready: 0, failed: 0 },
|
|
105
|
+
},
|
|
106
|
+
kinds: {},
|
|
107
|
+
last: [],
|
|
108
|
+
updated_at: nowIso(),
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** 计数位:只认有限非负整数,其余归零(防手改/坏文件把计数变成 NaN)。 */
|
|
113
|
+
function countOf(v: unknown): number {
|
|
114
|
+
return typeof v === 'number' && Number.isFinite(v) && v >= 0 ? Math.floor(v) : 0
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** 盘面内容 → 状态:逐字段归并,形态不符的字段一律丢弃(不信任盘面内容)。 */
|
|
118
|
+
function mergeState(raw: unknown): HookAuditState {
|
|
119
|
+
const s = emptyState()
|
|
120
|
+
if (!raw || typeof raw !== 'object') return s
|
|
121
|
+
const r = raw as { counts?: unknown; kinds?: unknown; last?: unknown }
|
|
122
|
+
if (r.counts && typeof r.counts === 'object') {
|
|
123
|
+
const c = r.counts as { filtered?: unknown; written?: unknown; skipped?: unknown }
|
|
124
|
+
const buckets: Array<[Record<string, number>, string[], unknown]> = [
|
|
125
|
+
[s.counts.filtered as Record<string, number>, FILTER_REASONS, c.filtered],
|
|
126
|
+
[s.counts.written as Record<string, number>, WRITE_ROLES, c.written],
|
|
127
|
+
[s.counts.skipped as Record<string, number>, SKIP_REASONS, c.skipped],
|
|
128
|
+
]
|
|
129
|
+
for (const [dst, keys, src] of buckets) {
|
|
130
|
+
if (!src || typeof src !== 'object') continue
|
|
131
|
+
for (const key of keys) dst[key] = countOf((src as Record<string, unknown>)[key])
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (r.kinds && typeof r.kinds === 'object') {
|
|
135
|
+
for (const [k, v] of Object.entries(r.kinds as Record<string, unknown>)) {
|
|
136
|
+
if (typeof v === 'number' && Number.isFinite(v) && v > 0) s.kinds[k] = Math.floor(v)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (Array.isArray(r.last)) {
|
|
140
|
+
for (const e of r.last) {
|
|
141
|
+
if (!e || typeof e !== 'object') continue
|
|
142
|
+
const entry = e as Partial<HookAuditEntry>
|
|
143
|
+
if (typeof entry.ts !== 'string') continue
|
|
144
|
+
if (entry.action !== 'filtered' && entry.action !== 'written' && entry.action !== 'skipped') continue
|
|
145
|
+
s.last.push(entry as HookAuditEntry)
|
|
146
|
+
}
|
|
147
|
+
if (s.last.length > MAX_LAST) s.last = s.last.slice(-MAX_LAST)
|
|
148
|
+
}
|
|
149
|
+
return s
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 有界落盘审计记录器(**静默降级**:任何公共方法都不会抛错)。
|
|
154
|
+
*
|
|
155
|
+
* 用法(hooks.ts 内):`new HookAuditRecorder(opts.auditPath)`,随后在
|
|
156
|
+
* 三处过滤分支与写入/跳过路径各调一次对应方法。审计失败只影响审计自身。
|
|
157
|
+
*/
|
|
158
|
+
export class HookAuditRecorder {
|
|
159
|
+
/** 实际生效的审计文件路径(构造时定死,供日志/测试核对)。 */
|
|
160
|
+
readonly path: string
|
|
161
|
+
/** 进程内状态缓存(首次记录时从盘面载入,此后只增量改内存 + 全量重写)。 */
|
|
162
|
+
#state: HookAuditState | null = null
|
|
163
|
+
|
|
164
|
+
constructor(path?: string) {
|
|
165
|
+
this.path = typeof path === 'string' && path.trim() ? path.trim() : DEFAULT_HOOK_AUDIT_PATH
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** 一条**被设计滤除**的消息(三处过滤分支各调一次)。 */
|
|
169
|
+
filtered(reason: HookFilterReason, kind?: string): void {
|
|
170
|
+
this.#mutate((s) => {
|
|
171
|
+
s.counts.filtered[reason] += 1
|
|
172
|
+
s.last.push(kind === undefined
|
|
173
|
+
? { ts: nowIso(), action: 'filtered', reason }
|
|
174
|
+
: { ts: nowIso(), action: 'filtered', reason, kind })
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** `source.kind` 分布(每条**进入消息级处理**的事件调一次,含被滤与放行)。 */
|
|
179
|
+
observeKind(kind: string): void {
|
|
180
|
+
this.#mutate((s) => {
|
|
181
|
+
const key = this.#boundedKindKey(s, kind)
|
|
182
|
+
s.kinds[key] = (s.kinds[key] ?? 0) + 1
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** 成功**发起**的写入(按 role;发起后失败另记 skipped.failed)。 */
|
|
187
|
+
written(role: HookWriteRole): void {
|
|
188
|
+
this.#mutate((s) => {
|
|
189
|
+
s.counts.written[role] += 1
|
|
190
|
+
s.last.push({ ts: nowIso(), action: 'written', role })
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** 跳过写入(脱敏置空 / 认知图未就绪 / 写入失败)。 */
|
|
195
|
+
skipped(reason: HookSkipReason, role?: HookWriteRole): void {
|
|
196
|
+
this.#mutate((s) => {
|
|
197
|
+
s.counts.skipped[reason] += 1
|
|
198
|
+
s.last.push(role === undefined
|
|
199
|
+
? { ts: nowIso(), action: 'skipped', reason }
|
|
200
|
+
: { ts: nowIso(), action: 'skipped', reason, role })
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** 键数有界:已见键原样用;新键在未达上限时收录,达限后统一归并到 "other"。 */
|
|
205
|
+
#boundedKindKey(s: HookAuditState, kind: string): string {
|
|
206
|
+
if (Object.prototype.hasOwnProperty.call(s.kinds, kind)) return kind
|
|
207
|
+
return Object.keys(s.kinds).length >= MAX_KINDS ? OTHER_KIND : kind
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** 读改写 + 原子落盘;整段 try/catch——审计失败静默降级(绝不冒泡进记忆路径)。 */
|
|
211
|
+
#mutate(fn: (s: HookAuditState) => void): void {
|
|
212
|
+
try {
|
|
213
|
+
const s = this.#state ?? (this.#state = this.#load())
|
|
214
|
+
fn(s)
|
|
215
|
+
if (s.last.length > MAX_LAST) s.last = s.last.slice(-MAX_LAST)
|
|
216
|
+
s.updated_at = nowIso()
|
|
217
|
+
this.#save(s)
|
|
218
|
+
} catch { /* 审计失败静默降级:记忆路径必须照常 */ }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** 盘面 → 状态;不存在/不可读/坏 JSON/形态不符 → 空态重建(不抛)。 */
|
|
222
|
+
#load(): HookAuditState {
|
|
223
|
+
try {
|
|
224
|
+
return mergeState(JSON.parse(readFileSync(this.path, 'utf8')) as unknown)
|
|
225
|
+
} catch {
|
|
226
|
+
return emptyState()
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** 原子写:同目录临时文件 + rename(临时文件在 rename 失败时清理)。 */
|
|
231
|
+
#save(s: HookAuditState): void {
|
|
232
|
+
const dir = dirname(this.path)
|
|
233
|
+
mkdirSync(dir, { recursive: true })
|
|
234
|
+
const tmp = `${this.path}.${process.pid.toString(36)}.${Date.now().toString(36)}.tmp`
|
|
235
|
+
writeFileSync(tmp, `${JSON.stringify(s, null, 2)}\n`, 'utf8')
|
|
236
|
+
try {
|
|
237
|
+
renameSync(tmp, this.path)
|
|
238
|
+
} catch (err) {
|
|
239
|
+
try { unlinkSync(tmp) } catch { /* 清理失败忽略:残留 .tmp 不影响读路径 */ }
|
|
240
|
+
throw err
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
package/src/lib/mutual.ts
CHANGED
|
@@ -259,8 +259,12 @@ export function combineVerdict(w: VerifyResult['whitebox'],
|
|
|
259
259
|
l: VerifyResult['llm_review']): VerifyResult['verdict'] {
|
|
260
260
|
const c = l.conclusion || ''
|
|
261
261
|
// P1 修复(GPT 审查):白箱通道异常 → fail-closed(不因复核「同意」而 pass)
|
|
262
|
+
// N268:**复核通道**异常的标记由 llmReview 写在 `l.conclusion`(`llm_error: …`)——
|
|
263
|
+
// 此前只查 w.judgment(该字段只可能由 whiteboxVerify 写 whitebox_error)⇒ 该分支
|
|
264
|
+
// 是死条件,复核抛异常时白箱「采纳」径直判 pass(复核不可用反成「通过」)。
|
|
265
|
+
// 两字段都查:l 侧是真正的生产者口径,w 侧保留同形防御(防上游把异常塞进白箱字段)。
|
|
262
266
|
if (w.judgment.startsWith('whitebox_error') || w.judgment.startsWith('llm_error')
|
|
263
|
-
|| w.d_norm < 0) {
|
|
267
|
+
|| c.startsWith('llm_error') || w.d_norm < 0) {
|
|
264
268
|
return 'needs_revision'
|
|
265
269
|
}
|
|
266
270
|
if (w.judgment.startsWith('采纳')) {
|
package/src/lib/roleplay_web.ts
CHANGED
|
@@ -847,6 +847,17 @@ export async function installRoleplayWeb(ctx, capability, config, disposers, mdc
|
|
|
847
847
|
res.end(JSON.stringify({ ok: false, error: '非法角色 id(白名单 ^[A-Za-z0-9_.-]{1,64}$)' }));
|
|
848
848
|
return;
|
|
849
849
|
}
|
|
850
|
+
// N266:入口类型闸——message 必须是字符串。此前这段直接
|
|
851
|
+
// `p.message.includes(w)`:message 为数组时静默走
|
|
852
|
+
// Array.prototype.includes(逐元素**全等**比较),把敏感词拆进
|
|
853
|
+
// 不同元素即整条绕过硬拦截;数字/对象/缺省则抛 TypeError 落 500
|
|
854
|
+
// (内部异常文本随响应外泄)。与 role_id 同口径:非法形态在入口
|
|
855
|
+
// 一律 400 拒——不转发引擎、不落转录。
|
|
856
|
+
if (typeof p.message !== 'string') {
|
|
857
|
+
res.writeHead(400, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
858
|
+
res.end(JSON.stringify({ ok: false, route: 'refused', refused: 'bad_message', error: 'message 必须是字符串(非字符串形态不得绕过内容硬拦截,N266)' }));
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
850
861
|
// P1 完善(会话隔离):按客户端实例隔离 session 与转录
|
|
851
862
|
const cid = clientIdOf(req);
|
|
852
863
|
// —— 服务端内容分级硬拦截(不依赖前端,法律与协议保护)——
|
|
@@ -860,13 +871,17 @@ export async function installRoleplayWeb(ctx, capability, config, disposers, mdc
|
|
|
860
871
|
res.end(JSON.stringify({ reply: '⛔ 已拒绝:涉及未成年人的性内容违反国家法律与灵枢协议(未成年人保护)。灵枢不提供任何涉及未成年人的性扮演内容。', route: 'refused', refused: 'minor_nsfw' }));
|
|
861
872
|
return;
|
|
862
873
|
}
|
|
863
|
-
appendTranscript(role, { time: Date.now(), role: 'user', text: p.message }, cid);
|
|
864
|
-
void toGraph('user-turn', (g) => g.writeTranscript(role, cid, 'user', p.message));
|
|
865
874
|
if (!capReady()) {
|
|
866
875
|
res.writeHead(503, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
867
876
|
res.end(JSON.stringify({ reply: '', route: 'unavailable', error: CAP_ABSENT_MSG }));
|
|
868
877
|
return;
|
|
869
878
|
}
|
|
879
|
+
// N266:落盘移到内容门控与就绪闸**之后**——被内容门控拒绝的、以及
|
|
880
|
+
// 「能力未接入」(503)的回合都从未发生,此前这两笔写在校验与
|
|
881
|
+
// capReady 之前,未经门控的 message 原文会被留进本地转录
|
|
882
|
+
//(读同一文件的历史接口随即原样回放)。
|
|
883
|
+
appendTranscript(role, { time: Date.now(), role: 'user', text: p.message }, cid);
|
|
884
|
+
void toGraph('user-turn', (g) => g.writeTranscript(role, cid, 'user', p.message));
|
|
870
885
|
const r = await capability.callTool('roleplay_chat', {
|
|
871
886
|
message: p.message, role_id: role, session_id: sessionFor(role, cid), data_dir: roleDataDir,
|
|
872
887
|
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session_state.ts —— 插件侧「运行期会话状态」单点(观测 → 写归因注入)
|
|
3
|
+
*
|
|
4
|
+
* 为什么有这个模块(B 治本批,2026-10-06)
|
|
5
|
+
* ----------------------------------------
|
|
6
|
+
* DSH 单进程多会话:宿主的会话标识只经 `session/event` 到达插件。此前它只被
|
|
7
|
+
* 用在 hooks 面(自动记忆的 remember / read 带 extra.session);**agent 直调
|
|
8
|
+
* 工具面**(src/tools.ts 的 execute → bridge.callTool)转发时**不带会话**——
|
|
9
|
+
* 而部署侧 `MDCG_SESSION` env 一旦取消/为空,`md_cg/mcp_server.py` 的
|
|
10
|
+
* `_declared_session`(:3823-3850)就以**请求声明**为归因来源
|
|
11
|
+
* (优先级:env > 请求声明 > 进程身份),不传即落回进程身份——md_cg 的
|
|
12
|
+
* `Principal.__init__` 会为假值 session 生成**进程级随机** `sess_<hex>`
|
|
13
|
+
* (md_cg/security.py:117):归属在审计上既读不出是谁、跨进程也对不上,
|
|
14
|
+
* 是静默的归属丢失。
|
|
15
|
+
*
|
|
16
|
+
* 本模块把「观测会话」与「写归因注入」收成**单点**:
|
|
17
|
+
* · hooks 面:每收到 session/event 就 `noteSession(sid)`——调用点仍在 H1
|
|
18
|
+
* 子代理闸**之后**(子代理会话不得污染会话状态的位置不变量保持);
|
|
19
|
+
* · 工具面:写归因调用注入 `currentSession() || UNASSIGNED_SESSION`
|
|
20
|
+
* (判据矩阵见 `attributeSession`,只注入写面;读面一律不注入)。
|
|
21
|
+
*
|
|
22
|
+
* 模块级状态(`lastSession`)是**进程级**的:同一进程内多个会话共享它,
|
|
23
|
+
* 「最近一次观测」即当前活动会话——与 hooks 面的既有口径一致(见
|
|
24
|
+
* test/session-attribution.test.ts ④「取值每步稳定」)。
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** 会话归属未知时的**显式占位**(H2③,2026-09-30;原定义在 src/hooks.ts,
|
|
28
|
+
* B 治本批迁入本单点——注释要点保真搬迁)。
|
|
29
|
+
*
|
|
30
|
+
* ⚠️ 不可退回「不传 session 键」:md_cg 的 `Principal.__init__` 在 session 为假值时
|
|
31
|
+
* 生成**进程级随机** `sess_<hex>`(md_cg/security.py:117)——插件不传,等于让一个
|
|
32
|
+
* 进程内所有「宿主未给标识」的会话共用一个**不可辨认**的随机桶:归属在审计上既
|
|
33
|
+
* 读不出是谁、跨进程也对不上,是静默的归属丢失。
|
|
34
|
+
* 本常量把这一态写成**显式值**:跨进程一致、可辨认、可审计,且不是伪造的宿主
|
|
35
|
+
* 会话 id(非 DSH 形态,服务端 `_normalize_session` 原样采用、不会被改写成别的桶)。
|
|
36
|
+
* 要读这个桶:`stg(op=timeline, session="unassigned")`。 */
|
|
37
|
+
export const UNASSIGNED_SESSION = 'unassigned'
|
|
38
|
+
|
|
39
|
+
/** 最近一次观测到的宿主会话标识(空串 = 未观测到会话)。
|
|
40
|
+
*
|
|
41
|
+
* ⚠️ 本状态是**模块级(进程级)单点**:hooks 面观测写入(src/hooks.ts 的
|
|
42
|
+
* session/event),工具面读它做写归因注入(src/tools.ts 的转发面)——两处必须
|
|
43
|
+
* 同源,否则注入的会话与写入的归属对不上(B 治本批的目的即在此)。
|
|
44
|
+
*
|
|
45
|
+
* 「本实例是否观测过」的**实例级门不在本模块**,在 src/hooks.ts 的
|
|
46
|
+
* installMemoryHooks 内(局部 `observedSession`):即「本实例观测到会话之后」
|
|
47
|
+
* 才用本单点值回落召回过滤。这是**经 owner 裁定的契约字面偏离(dwfq-7b3a555e-1)**
|
|
48
|
+
* ——契约建议的形态是 hooks 侧字面 `|| currentSession()`,但字面形态与硬边界
|
|
49
|
+
* 「test/session-attribution.test.ts 逐字未动且全绿」互斥(该守卫 ② 以
|
|
50
|
+
* 「新建 harness = 未观测」为前提,字面回落会读成前一实例的 sess_B)。保留
|
|
51
|
+
* 实例门的两条理由:
|
|
52
|
+
* ① 「新实例 = 干净状态」是原闭包变量 `lastSession` 的**有意属性**(新装的钩子
|
|
53
|
+
* 在观测到会话前,自动召回不加过滤)——实例隔离语义在测试面被真实保留;
|
|
54
|
+
* ② 与既有冻结守卫 ② 相容(守卫逐字未动且全绿是本批硬边界)。
|
|
55
|
+
* 真机单实例下两者**逐位等价**:唯一差异窗口是「本实例未观测 ∧ 模块单点非空」,
|
|
56
|
+
* 而模块值只由本实例的 hooks 观测写入,单实例下该窗口不可达;HMR 重载场景下新
|
|
57
|
+
* 实例回落 '' 而非上一会话值,属更保守的防御行为(已由 owner 裁定接受)。 */
|
|
58
|
+
let lastSession = ''
|
|
59
|
+
|
|
60
|
+
/** 记录一次会话观测(H2③ 观测面单点)。
|
|
61
|
+
*
|
|
62
|
+
* trim 后非空才写入——空串/纯空白**不覆盖**旧值(否则「宿主给了一次空标识」
|
|
63
|
+
* 会把已观测到的真会话抹掉,后续注入退化为 unassigned)。 */
|
|
64
|
+
export function noteSession(sid: string): void {
|
|
65
|
+
const s = sid.trim()
|
|
66
|
+
if (s) lastSession = s
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 当前运行期会话标识(空串 = 未观测到;调用方按 `|| UNASSIGNED_SESSION` 兜底)。 */
|
|
70
|
+
export function currentSession(): string {
|
|
71
|
+
return lastSession
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 写归因面判据:本次调用是否属于「要注入运行期会话」的写归因调用。
|
|
75
|
+
*
|
|
76
|
+
* **恰两条命中路径**(不得放宽、不得收窄出契约外):
|
|
77
|
+
* ① `mdcg_remember` —— md_cg 细粒度写入口(插件侧自动记忆/落图的写通道);
|
|
78
|
+
* ② `cg` 且 `op === 'write'` —— 认知图基元的带审核写路径。
|
|
79
|
+
*
|
|
80
|
+
* 为什么**只**这两个面(判据出处:md_cg/mcp_server.py):
|
|
81
|
+
* · **读面一律不注入**——`mdcg_recall` / `mdcg_search` / `mdcg_get` / `stg`
|
|
82
|
+
* 以及 `cg` 的其它 op,其 `session` 是**视图过滤**:服务端 schema 描述原文
|
|
83
|
+
* 「会话归属过滤(frontmatter.session;…缺省不过滤)」
|
|
84
|
+
* (md_cg/mcp_server.py:218-219 / :251-252 / :908-912 / :1074-1077)。
|
|
85
|
+
* 注入会把文档化的「缺省跨会话」翻转成「本会话视图」——那是功能收窄,
|
|
86
|
+
* 不是本批目标(读面要跨会话视图请显式 `stg(op=timeline, session="*")`)。
|
|
87
|
+
* · **op 特化语义不动**——`cg` 的 `sustain`/`session` 等 op 的 `session` 是
|
|
88
|
+
* 特化语义(resume/note 的**目标会话**),注入即污染其目标参数。
|
|
89
|
+
* · 归因与授权正交(issue #35 定稿):`call_tool` 的请求级 session **只做
|
|
90
|
+
* 归因**(写入归属/`_attribution` 取它),**不**改 `principal.session`
|
|
91
|
+
* (md_cg/mcp_server.py:3261-3264)——绑定档(private/secret)的读授权
|
|
92
|
+
* 锚定连接级身份,调用方自报的会话不构成看他人 private 的授权。
|
|
93
|
+
* 绑定档可见性判定 `MdCGSecure._readable`(md_cg/mdcos.py:5009-5053)
|
|
94
|
+
* 的会话绑定分支恒用 `nsess == self.principal.session`(:5051-5052),
|
|
95
|
+
* can_admin(设计者)豁免(:5045)——故本注入对绑定档无回归。
|
|
96
|
+
*
|
|
97
|
+
* ⚠️ 禁止把本判据放宽成「所有带 session 参数的工具」:那会把上面两类语义
|
|
98
|
+
* (读面视图过滤 / op 特化目标)一并改写,属越权改契约。
|
|
99
|
+
*/
|
|
100
|
+
function isWriteAttributionCall(toolName: string, args: Record<string, unknown>): boolean {
|
|
101
|
+
if (toolName === 'mdcg_remember') return true
|
|
102
|
+
return toolName === 'cg' && args['op'] === 'write'
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** 请求是否已显式声明会话(string 且 trim 后非空 → 保留调用方声明,绝不覆盖)。 */
|
|
106
|
+
function hasDeclaredSession(args: Record<string, unknown>): boolean {
|
|
107
|
+
const v = args['session']
|
|
108
|
+
return typeof v === 'string' && v.trim() !== ''
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 把**运行期会话**注入**写归因调用**(args 的 session 键不可用时才注入)。
|
|
112
|
+
*
|
|
113
|
+
* 注入条件:`session` 键缺失 / 非 string 类型 / trim 后空串 ⇒ 注入
|
|
114
|
+
* (`null`/`''`/空白一律视为「未声明」——服务端 `_declared_session` 对假值
|
|
115
|
+
* 同样解析为无归属声明,原样转发只会落回进程级随机 sess_* 兜底桶);
|
|
116
|
+
* 显式非空声明一律**不覆盖**(调用方自报优先——插件不替调用方改归属)。
|
|
117
|
+
*
|
|
118
|
+
* 注入值:`currentSession() || UNASSIGNED_SESSION`(未观测到会话时用
|
|
119
|
+
* 'unassigned' 显式占位——与 hooks 面 H2③ 同口径)。
|
|
120
|
+
*
|
|
121
|
+
* 命中且需注入 → 返回**新对象** `{ ...args, session: v }`(绝不 mutate 输入);
|
|
122
|
+
* 否则**原样返回**(同一引用)——未命中的调用零开销、零可观察差异。 */
|
|
123
|
+
export function attributeSession(toolName: string, args: Record<string, unknown>): Record<string, unknown> {
|
|
124
|
+
if (!isWriteAttributionCall(toolName, args)) return args
|
|
125
|
+
if (hasDeclaredSession(args)) return args
|
|
126
|
+
return { ...args, session: currentSession() || UNASSIGNED_SESSION }
|
|
127
|
+
}
|
package/src/tools.ts
CHANGED
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
import type { Context } from '@deepseek-ai/cordis'
|
|
9
9
|
import { defineTool, type ParameterPropertySpec, type ParameterSchemaSpec, type ValueSchemaSpec } from '@deepseek-ai/dsh-tools'
|
|
10
10
|
import type { LingshuBridge, McpTool } from './bridge.ts'
|
|
11
|
+
// B 治本批(2026-10-06):写归因注入单点(运行期会话 → 写归因调用)。
|
|
12
|
+
// 运行时导入写 `.js`(本仓口径:type-only 才写 `.ts`,tsconfig 未开
|
|
13
|
+
// allowImportingTsExtensions —— 值导入写 `.ts` 会 TS5097 编译失败)。
|
|
14
|
+
import { attributeSession } from './lib/session_state.js'
|
|
11
15
|
|
|
12
16
|
/** 默认暴露的核心工具集合:**记忆面已基元化**,只注册 `cg` / `stg` 两个认知基元。
|
|
13
17
|
*
|
|
@@ -186,7 +190,14 @@ export async function registerLingshuTools(
|
|
|
186
190
|
// 此前完全忽略取消,取消后写操作(remember/relate/ingest 等)仍可能产生副作用
|
|
187
191
|
async execute(args: Record<string, unknown>, exec: { signal: AbortSignal }) {
|
|
188
192
|
if (exec.signal.aborted) throw new Error(`灵枢 ${tool.name} 已取消`)
|
|
189
|
-
|
|
193
|
+
// B 治本批(2026-10-06):agent 直调工具的转发面把**运行期会话**注入
|
|
194
|
+
// **写归因调用**——env(MDCG_SESSION)取消后,请求声明即归因唯一来源
|
|
195
|
+
// (md_cg/mcp_server.py 的 _declared_session:env > 请求声明 > 进程身份)。
|
|
196
|
+
// 判据单点在 lib/session_state.ts 的 attributeSession:只注入
|
|
197
|
+
// mdcg_remember 与 cg(op=write) 两个写面;读面(视图过滤)/ op 特化语义
|
|
198
|
+
// 一律不动。命中且未显式声明时返回新对象,否则原样透传。
|
|
199
|
+
const forwarded = attributeSession(tool.name, args as Record<string, unknown>)
|
|
200
|
+
const result = await bridge.callTool(tool.name, forwarded, exec.signal)
|
|
190
201
|
if (exec.signal.aborted) throw new Error(`灵枢 ${tool.name} 已取消`)
|
|
191
202
|
if (result.isError) {
|
|
192
203
|
throw new Error(extractText(result.content) || `灵枢 ${tool.name} 执行失败`)
|
package/zcode/AGENTS.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
> 本文件由 `scripts/render_discipline.py` 从真源
|
|
4
4
|
> `docs/工作纪律_认知图条目_v1.1.json` 渲染生成,**请勿手改**;改真源后重跑渲染。
|
|
5
|
-
> 真源指纹(SHA256 前16位):
|
|
5
|
+
> 真源指纹(SHA256 前16位):e8c979edba52de41
|
|
6
6
|
|
|
7
7
|
## 概述
|
|
8
8
|
|
|
9
|
-
工作纪律集: 4条方法论(理论先行/全面处理/白箱方法/根因) + 3条执行(验证/双副本/兜底) + 1条思考(中文思考/协作模式分级) + 1条保密(敏感信息隔离) + 3条图像线(选源护栏/历史查询优先/算法权威唯一) + 1条协作(访谈澄清:先问清再动手) + 1条合规(内容政策合规:公开产物过内容政策+隐私双清单) + 2条执行调度与运行环境(命令执行统一走python/UTF-8: 显式encoding+PYTHONUTF8=1, 规避GBK, 全仓文本与路径一律UTF-8(无BOM, 中文可进路径), 不做控制台兼容(取用走python); 任务派发统一走蜂巢(zcode端的工作流工具为等效蜂巢工具——使用者2026-10-04裁定; 蜂巢功能完全完善之前可优先使用zcode工作流): 任何执行性任务经蜂巢spawn/submit执行并留痕, agent本体只做编排, L1出口=产物落点为只读判定(不改仓库/外部状态)可直跑须留痕) + 1条记忆闭环(任务收尾归档:每次任务执行完只将核心修改(内容/原因/位置/验证结论)按CCG六要素(功能名/生效条件/子功能/执行/验证方式/不适用条件)成文存入灵枢记忆, 不写中间过程; text 类缺要素即被写入闸门拒并当场返回缺失清单) + 1条信息获取(工作区索引优先: 查工作区文件先读 WORKSPACE_INDEX.md(仓根, 由 scripts/workspace_index.py 从 git 追踪面管线生成), 尚无则先跑 --write 生成再读, 不以重复全盘浏览代替; 索引是会陈化的手工件, 故须管线生成+机械守卫, 否则退化为误导源)
|
|
9
|
+
工作纪律集: 4条方法论(理论先行/全面处理/白箱方法/根因) + 3条执行(验证/双副本/兜底) + 1条思考(中文思考/协作模式分级) + 1条保密(敏感信息隔离) + 3条图像线(选源护栏/历史查询优先/算法权威唯一) + 1条协作(访谈澄清:先问清再动手) + 1条合规(内容政策合规:公开产物过内容政策+隐私双清单) + 2条执行调度与运行环境(命令执行统一走python/UTF-8: 显式encoding+PYTHONUTF8=1, 规避GBK, 全仓文本与路径一律UTF-8(无BOM, 中文可进路径), 不做控制台兼容(取用走python), 公开面路径写相对路径(守 check_local_paths); 任务派发统一走蜂巢(zcode端的工作流工具为等效蜂巢工具——使用者2026-10-04裁定; 蜂巢功能完全完善之前可优先使用zcode工作流): 任何执行性任务经蜂巢spawn/submit执行并留痕, agent本体只做编排, L1出口=产物落点为只读判定(不改仓库/外部状态)可直跑须留痕) + 1条记忆闭环(任务收尾归档:每次任务执行完只将核心修改(内容/原因/位置/验证结论)按CCG六要素(功能名/生效条件/子功能/执行/验证方式/不适用条件)成文存入灵枢记忆, 不写中间过程; text 类缺要素即被写入闸门拒并当场返回缺失清单) + 1条信息获取(工作区索引优先: 查工作区文件先读 WORKSPACE_INDEX.md(仓根, 由 scripts/workspace_index.py 从 git 追踪面管线生成), 尚无则先跑 --write 生成再读, 不以重复全盘浏览代替; 索引是会陈化的手工件, 故须管线生成+机械守卫, 否则退化为误导源)
|
|
10
10
|
|
|
11
11
|
## 执行公约
|
|
12
12
|
|
|
@@ -120,10 +120,10 @@
|
|
|
120
120
|
|
|
121
121
|
### 15. 命令执行统一走python
|
|
122
122
|
|
|
123
|
-
-
|
|
124
|
-
- **动作**:构造 argv 列表→subprocess.run(capture_output=True, text=True, encoding='utf-8', errors='replace')→env 带 PYTHONUTF8=1→shell=False;边界判据=本次调用是否产出非UTF-8字节流(纯MSYS工具与不起进程的内置工具豁免;原生Windows程序不豁免,须python包装或显式解码)
|
|
123
|
+
- **触发**:执行命令|运行测试/脚本|跨进程读写文本|书写文档/引用路径
|
|
124
|
+
- **动作**:构造 argv 列表→subprocess.run(capture_output=True, text=True, encoding='utf-8', errors='replace')→env 带 PYTHONUTF8=1→shell=False;边界判据=本次调用是否产出非UTF-8字节流(纯MSYS工具与不起进程的内置工具豁免;原生Windows程序不豁免,须python包装或显式解码);仓内引用与文档路径一律写相对路径(本机绝对路径以中性占位代), 判据 scripts/check_local_paths.py
|
|
125
125
|
- **不适用**:IDE内置工具直调(判据=本次调用是否产出非UTF-8字节流;不起进程的内置工具(Read/Write/Edit/Grep/Glob/WebFetch)与纯MSYS工具(grep/ls/sed)归入本豁免;拉起shell且执行原生Windows程序者(taskkill/wmic/npm.cmd/cargo/where)不属本豁免,仍须python包装或对输出显式解码;2026-09-28使用者裁定)|无跨进程的纯内存计算
|
|
126
|
-
- **声明**:按工作纪律第15条: 命令执行统一走python——argv列表+显式UTF-8+PYTHONUTF8=1, 不经Windows shell, 规避GBK解码异常。 另: 全仓文本与路径/文件名一律UTF-8(中文可进路径), 不做控制台兼容(取用走python, 控制台乱码属显示层)。
|
|
126
|
+
- **声明**:按工作纪律第15条: 命令执行统一走python——argv列表+显式UTF-8+PYTHONUTF8=1, 不经Windows shell, 规避GBK解码异常。 另: 全仓文本与路径/文件名一律UTF-8(中文可进路径), 不做控制台兼容(取用走python, 控制台乱码属显示层)。 另: 公开面路径写相对路径——本机绝对路径以中性占位代(仓内 `<仓根>`/家目录 `~/`/临时 `%TEMP%`), 门禁 check_local_paths。
|
|
127
127
|
|
|
128
128
|
### 16. 任务收尾归档(记忆闭环)
|
|
129
129
|
|
|
@@ -164,7 +164,7 @@
|
|
|
164
164
|
| 12 | 按工作纪律第12条: 算法权威唯一(线稿=darkline.py/色块=contour_semantics)——选算法查文档, 勿临时切换/漂移; 换算法须记录+更新文档。 |
|
|
165
165
|
| 13 | 按工作纪律第13条: 访谈澄清——先提问确认要做什么,逐轮问到完全清楚才动手;事实自查,决策归使用者,不完全清楚不固化。 |
|
|
166
166
|
| 14 | 按工作纪律第14条: 内容政策合规——对外公开产物必须过「内容政策+隐私」双清单,过滤在生成阶段做;已发布发现违规要删条+重写git历史+通知平台清缓存。 |
|
|
167
|
-
| 15 | 按工作纪律第15条: 命令执行统一走python——argv列表+显式UTF-8+PYTHONUTF8=1, 不经Windows shell, 规避GBK解码异常。 另: 全仓文本与路径/文件名一律UTF-8(中文可进路径), 不做控制台兼容(取用走python, 控制台乱码属显示层)。 |
|
|
167
|
+
| 15 | 按工作纪律第15条: 命令执行统一走python——argv列表+显式UTF-8+PYTHONUTF8=1, 不经Windows shell, 规避GBK解码异常。 另: 全仓文本与路径/文件名一律UTF-8(中文可进路径), 不做控制台兼容(取用走python, 控制台乱码属显示层)。 另: 公开面路径写相对路径——本机绝对路径以中性占位代(仓内 `<仓根>`/家目录 `~/`/临时 `%TEMP%`), 门禁 check_local_paths。 |
|
|
168
168
|
| 16 | 按工作纪律第16条: 任务收尾归档——每次任务执行完只提炼核心修改(内容/原因/位置/验证结论)并按 CCG 六要素(功能名/生效条件/子功能/执行/验证方式/不适用条件)成文存入灵枢记忆(text 类六要素缺失会被写入闸门当场拒绝并返回缺失清单——补齐后重写即可, 不必等读回确认才发现), 写入后发起一次读取查询确认写入成功且可检索, 禁写中间过程/试错/调试等无效信息, 与第2条形成「查记忆→执行→写记忆→读回确认」闭环。 |
|
|
169
169
|
| 17 | 按工作纪律第17条: 任务派发统一走蜂巢——任何执行性任务经蜂巢 spawn/submit 执行并留痕(spec/status/result), agent本体只做编排; 宿主自带subagent/team不是等价通道(zcode端的工作流工具为等效蜂巢工具——使用者2026-10-04裁定; 蜂巢功能完全完善之前, zcode端可优先使用zcode工作流处理), 兜底须声明。L1只读判定(产物落点=不改仓库/外部状态)可直跑, 须输出「L1 直跑:<命令> — 风险/频次/可逆性」留痕。 |
|
|
170
170
|
| 18 | 按工作纪律第18条: 工作区索引优先——查工作区文件先读 WORKSPACE_INDEX.md(仓根, 管线生成); 无则先跑 scripts/workspace_index.py --write 生成再读, 不以重复全盘浏览代替。 |
|