@furongjun1999/dsh-memory 0.7.3 → 0.7.5
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 +631 -629
- package/codebuddy/CODEBUDDY.md +5 -5
- package/docs/discipline/templates/zcode-user.md.tmpl +4 -1
- 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/217/221/345/270/20313_/344/270/212/344/270/213/346/226/207/350/207/252/347/256/241/347/220/206/346/234/272/345/210/266_v1.0.md +114 -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/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 -43
- 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 +80 -1
- package/docs/plans//347/201/265/346/236/242/350/272/253/344/275/223/303/227/350/204/221_/344/270/226/347/225/214/346/250/241/345/236/213/345/257/271/346/216/245/350/256/276/350/256/241_v0.1.md +76 -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 +461 -461
- package/docs//350/256/260/345/277/206/347/263/273/347/273/237/345/215/263/347/231/275/347/256/261/345/214/226/346/231/272/350/203/275_/346/236/266/346/236/204/345/257/271/347/205/247_v0.2.md +121 -0
- package/docs//350/256/260/345/277/206/347/263/273/347/273/237/345/215/263/347/231/275/347/256/261/345/214/226/346/231/272/350/203/275_/346/236/266/346/236/204/345/257/271/347/205/247_v0.3.md +117 -0
- package/lib/hooks.d.ts +34 -0
- package/lib/hooks.js +205 -18
- package/lib/index.js +22 -1
- package/lib/lib/mdcg_client.d.ts +37 -0
- package/lib/lib/mdcg_client.js +44 -0
- 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/audit.py +7 -1
- package/md_cg/bench_governance.py +4 -1
- package/md_cg/bench_locomo_zh.py +12 -2
- package/md_cg/bench_zh_mad.py +12 -2
- package/md_cg/interop.py +25 -9
- package/md_cg/lifecycle.py +38 -0
- package/md_cg/mcp_server.py +64 -11
- package/md_cg/mdcos.py +78 -9
- package/md_cg/sleep.py +223 -103
- package/md_cg/state_slots.py +221 -0
- package/md_cg/stg.py +68 -3
- package/md_cg/sustain.py +150 -4
- package/md_cg/test_lifecycle_retire_leak.py +408 -0
- package/md_cg/test_read_face_semantics.py +6 -3
- package/md_cg/test_state_event_op.py +514 -0
- package/md_cg/test_state_slots.py +691 -0
- package/md_cg/test_sustain_bounded.py +624 -0
- package/md_cg/tokens.py +8 -1
- package/md_cg/units.py +4 -1
- package/package.json +1 -1
- package/skills/plugin.json +1 -1
- package/src/hooks.ts +217 -17
- package/src/index.ts +22 -1
- package/src/lib/mdcg_client.ts +48 -0
- package/src/lib/session_state.ts +127 -0
- package/src/tools.ts +12 -1
- package/zcode/AGENTS.md +5 -5
package/lib/lib/mdcg_client.d.ts
CHANGED
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
* 外部裁决回填 → MdcgClient.verify() → MCP cg(op=verify)
|
|
20
20
|
* 最近记忆时间线 → MdcgClient.timeline() → MCP stg(op=timeline)
|
|
21
21
|
* 近期事件窗口 → MdcgClient.recent() → MCP cg(op=recent)
|
|
22
|
+
* 窗口追加事件 → MdcgClient.recentAdd() → MCP cg(op=recent, action=add)
|
|
23
|
+
* 会话续接包 → MdcgClient.sessionRecall() → MCP cg(op=session, action=recall)
|
|
22
24
|
* 身份读取 → MdcgClient.identity() → MCP cg(op=identity)
|
|
23
25
|
* 白箱能力验证 → MdcgClient.whitebox() → MCP cg(op=whitebox)
|
|
24
26
|
* 服务信息 → MdcgClient.serviceInfo()→ MCP cg(op=info)
|
|
@@ -182,6 +184,41 @@ export declare class MdcgClient {
|
|
|
182
184
|
verify(nodeId: string, evidence: string, verdict: string): Promise<unknown>;
|
|
183
185
|
/** 最近记忆。 */
|
|
184
186
|
recent(limit?: number): Promise<unknown>;
|
|
187
|
+
/** 追加一条**近期事件窗口**记录(`cg(op=recent, action=add)` →
|
|
188
|
+
* `md_cg` 的 `remember_event(role, text, tags, meta, window)`)。
|
|
189
|
+
*
|
|
190
|
+
* 这是「滑动窗口」写侧:事件落 `_recent.jsonl`(**运行态面**),按窗口滚动
|
|
191
|
+
* 淘汰(服务端缺省 200 条,`mdcg.py:91 DEFAULT_RECENT_WINDOW`)——它**不是
|
|
192
|
+
* 知识节点**:不占 knowledge 层、不进检索正排,与 `remember()` 的知识面沉淀
|
|
193
|
+
* 是两条独立的轨道(见 src/hooks.ts 文件头的 contextWindow 头注)。
|
|
194
|
+
*
|
|
195
|
+
* ⚠️ **不注入 as_unit**(与 `write()` 同款理由的反面):本调用不写任何层的
|
|
196
|
+
* 节点(服务端 `remember_event` 不做层白名单校验),收窄单元无收益且可能压低
|
|
197
|
+
* 事件密级(缺省 internal);`meta.session` 由调用方显式给出(沿本文件
|
|
198
|
+
* `remember()` 的 sessionTag 口径)。
|
|
199
|
+
*
|
|
200
|
+
* ⚠️ 服务端对 `meta` 做 `setdefault`(tenant/session/harness/unit,见
|
|
201
|
+
* `mdcos.py:5057 remember_event`):调用方已写的键**不被覆盖**。 */
|
|
202
|
+
recentAdd(role: string, text: string, meta?: Record<string, unknown>, tags?: string[]): Promise<unknown>;
|
|
203
|
+
/** **会话续接包**(`cg(op=session, action=recall)` → `md_cg` 的
|
|
204
|
+
* `session_recall`):一次取回 notes / goals / tasks / **recent 事件窗口** /
|
|
205
|
+
* unresolved / self_state,按 `budget_tokens` 整包裁剪(服务端
|
|
206
|
+
* `mdcos.py:3351-3362`:交替丢 recent / notes 尾部)。
|
|
207
|
+
*
|
|
208
|
+
* 本插件消费其中的 `recent` 段——滑动窗口的**注入面**(见 src/hooks.ts)。
|
|
209
|
+
* `recent_limit` 语义 = 近期事件**条数**(非「轮数」);服务端读取时会按
|
|
210
|
+
* `max(1, recent_limit or 10)` 归一(`mdcos.py:3309`)。
|
|
211
|
+
*
|
|
212
|
+
* ⚠️ 两条服务端事实(决定注入侧的可达性,勿据本方法名臆测):
|
|
213
|
+
* ① `recent` 段取 `recent_events(limit=recent_limit)` —— **不按 session
|
|
214
|
+
* 过滤**(近期事件是运行态滚动窗口,会话归属只写在每条事件的 meta 里);
|
|
215
|
+
* ② budget 是**整包**预算:库内 notes/tasks/self_state 占位越多,同样
|
|
216
|
+
* budget 下 recent 段被裁得越短(实测:空库 + 10 条窗口条目,
|
|
217
|
+
* budget_tokens=600 → recent 8 条;1200 → 10 条)。
|
|
218
|
+
*
|
|
219
|
+
* 只读调用,不注入 as_unit(同其它读路径:读无副作用,收窄只会压低 owner
|
|
220
|
+
* 的 private 读能力)。 */
|
|
221
|
+
sessionRecall(session: string, recentLimit: number, budgetTokens: number): Promise<unknown>;
|
|
185
222
|
/** 最近记忆**时间线**(AEIS `timeline` 的对应物):`stg(op=timeline)` →
|
|
186
223
|
* `{count, limit, items:[{id, layer, start, end, preview}]}`,按时间倒序。
|
|
187
224
|
*
|
package/lib/lib/mdcg_client.js
CHANGED
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
* 外部裁决回填 → MdcgClient.verify() → MCP cg(op=verify)
|
|
20
20
|
* 最近记忆时间线 → MdcgClient.timeline() → MCP stg(op=timeline)
|
|
21
21
|
* 近期事件窗口 → MdcgClient.recent() → MCP cg(op=recent)
|
|
22
|
+
* 窗口追加事件 → MdcgClient.recentAdd() → MCP cg(op=recent, action=add)
|
|
23
|
+
* 会话续接包 → MdcgClient.sessionRecall() → MCP cg(op=session, action=recall)
|
|
22
24
|
* 身份读取 → MdcgClient.identity() → MCP cg(op=identity)
|
|
23
25
|
* 白箱能力验证 → MdcgClient.whitebox() → MCP cg(op=whitebox)
|
|
24
26
|
* 服务信息 → MdcgClient.serviceInfo()→ MCP cg(op=info)
|
|
@@ -215,6 +217,48 @@ export class MdcgClient {
|
|
|
215
217
|
recent(limit = 20) {
|
|
216
218
|
return this.cg({ op: 'recent', limit });
|
|
217
219
|
}
|
|
220
|
+
/** 追加一条**近期事件窗口**记录(`cg(op=recent, action=add)` →
|
|
221
|
+
* `md_cg` 的 `remember_event(role, text, tags, meta, window)`)。
|
|
222
|
+
*
|
|
223
|
+
* 这是「滑动窗口」写侧:事件落 `_recent.jsonl`(**运行态面**),按窗口滚动
|
|
224
|
+
* 淘汰(服务端缺省 200 条,`mdcg.py:91 DEFAULT_RECENT_WINDOW`)——它**不是
|
|
225
|
+
* 知识节点**:不占 knowledge 层、不进检索正排,与 `remember()` 的知识面沉淀
|
|
226
|
+
* 是两条独立的轨道(见 src/hooks.ts 文件头的 contextWindow 头注)。
|
|
227
|
+
*
|
|
228
|
+
* ⚠️ **不注入 as_unit**(与 `write()` 同款理由的反面):本调用不写任何层的
|
|
229
|
+
* 节点(服务端 `remember_event` 不做层白名单校验),收窄单元无收益且可能压低
|
|
230
|
+
* 事件密级(缺省 internal);`meta.session` 由调用方显式给出(沿本文件
|
|
231
|
+
* `remember()` 的 sessionTag 口径)。
|
|
232
|
+
*
|
|
233
|
+
* ⚠️ 服务端对 `meta` 做 `setdefault`(tenant/session/harness/unit,见
|
|
234
|
+
* `mdcos.py:5057 remember_event`):调用方已写的键**不被覆盖**。 */
|
|
235
|
+
recentAdd(role, text, meta = {}, tags = []) {
|
|
236
|
+
return this.cg({ op: 'recent', action: 'add', role, text, meta, tags });
|
|
237
|
+
}
|
|
238
|
+
/** **会话续接包**(`cg(op=session, action=recall)` → `md_cg` 的
|
|
239
|
+
* `session_recall`):一次取回 notes / goals / tasks / **recent 事件窗口** /
|
|
240
|
+
* unresolved / self_state,按 `budget_tokens` 整包裁剪(服务端
|
|
241
|
+
* `mdcos.py:3351-3362`:交替丢 recent / notes 尾部)。
|
|
242
|
+
*
|
|
243
|
+
* 本插件消费其中的 `recent` 段——滑动窗口的**注入面**(见 src/hooks.ts)。
|
|
244
|
+
* `recent_limit` 语义 = 近期事件**条数**(非「轮数」);服务端读取时会按
|
|
245
|
+
* `max(1, recent_limit or 10)` 归一(`mdcos.py:3309`)。
|
|
246
|
+
*
|
|
247
|
+
* ⚠️ 两条服务端事实(决定注入侧的可达性,勿据本方法名臆测):
|
|
248
|
+
* ① `recent` 段取 `recent_events(limit=recent_limit)` —— **不按 session
|
|
249
|
+
* 过滤**(近期事件是运行态滚动窗口,会话归属只写在每条事件的 meta 里);
|
|
250
|
+
* ② budget 是**整包**预算:库内 notes/tasks/self_state 占位越多,同样
|
|
251
|
+
* budget 下 recent 段被裁得越短(实测:空库 + 10 条窗口条目,
|
|
252
|
+
* budget_tokens=600 → recent 8 条;1200 → 10 条)。
|
|
253
|
+
*
|
|
254
|
+
* 只读调用,不注入 as_unit(同其它读路径:读无副作用,收窄只会压低 owner
|
|
255
|
+
* 的 private 读能力)。 */
|
|
256
|
+
sessionRecall(session, recentLimit, budgetTokens) {
|
|
257
|
+
return this.cg({
|
|
258
|
+
op: 'session', action: 'recall',
|
|
259
|
+
session, recent_limit: recentLimit, budget_tokens: budgetTokens,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
218
262
|
/** 最近记忆**时间线**(AEIS `timeline` 的对应物):`stg(op=timeline)` →
|
|
219
263
|
* `{count, limit, items:[{id, layer, start, end, preview}]}`,按时间倒序。
|
|
220
264
|
*
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
/** 会话归属未知时的**显式占位**(H2③,2026-09-30;原定义在 src/hooks.ts,
|
|
27
|
+
* B 治本批迁入本单点——注释要点保真搬迁)。
|
|
28
|
+
*
|
|
29
|
+
* ⚠️ 不可退回「不传 session 键」:md_cg 的 `Principal.__init__` 在 session 为假值时
|
|
30
|
+
* 生成**进程级随机** `sess_<hex>`(md_cg/security.py:117)——插件不传,等于让一个
|
|
31
|
+
* 进程内所有「宿主未给标识」的会话共用一个**不可辨认**的随机桶:归属在审计上既
|
|
32
|
+
* 读不出是谁、跨进程也对不上,是静默的归属丢失。
|
|
33
|
+
* 本常量把这一态写成**显式值**:跨进程一致、可辨认、可审计,且不是伪造的宿主
|
|
34
|
+
* 会话 id(非 DSH 形态,服务端 `_normalize_session` 原样采用、不会被改写成别的桶)。
|
|
35
|
+
* 要读这个桶:`stg(op=timeline, session="unassigned")`。 */
|
|
36
|
+
export declare const UNASSIGNED_SESSION = "unassigned";
|
|
37
|
+
/** 记录一次会话观测(H2③ 观测面单点)。
|
|
38
|
+
*
|
|
39
|
+
* trim 后非空才写入——空串/纯空白**不覆盖**旧值(否则「宿主给了一次空标识」
|
|
40
|
+
* 会把已观测到的真会话抹掉,后续注入退化为 unassigned)。 */
|
|
41
|
+
export declare function noteSession(sid: string): void;
|
|
42
|
+
/** 当前运行期会话标识(空串 = 未观测到;调用方按 `|| UNASSIGNED_SESSION` 兜底)。 */
|
|
43
|
+
export declare function currentSession(): string;
|
|
44
|
+
/** 把**运行期会话**注入**写归因调用**(args 的 session 键不可用时才注入)。
|
|
45
|
+
*
|
|
46
|
+
* 注入条件:`session` 键缺失 / 非 string 类型 / trim 后空串 ⇒ 注入
|
|
47
|
+
* (`null`/`''`/空白一律视为「未声明」——服务端 `_declared_session` 对假值
|
|
48
|
+
* 同样解析为无归属声明,原样转发只会落回进程级随机 sess_* 兜底桶);
|
|
49
|
+
* 显式非空声明一律**不覆盖**(调用方自报优先——插件不替调用方改归属)。
|
|
50
|
+
*
|
|
51
|
+
* 注入值:`currentSession() || UNASSIGNED_SESSION`(未观测到会话时用
|
|
52
|
+
* 'unassigned' 显式占位——与 hooks 面 H2③ 同口径)。
|
|
53
|
+
*
|
|
54
|
+
* 命中且需注入 → 返回**新对象** `{ ...args, session: v }`(绝不 mutate 输入);
|
|
55
|
+
* 否则**原样返回**(同一引用)——未命中的调用零开销、零可观察差异。 */
|
|
56
|
+
export declare function attributeSession(toolName: string, args: Record<string, unknown>): Record<string, unknown>;
|
|
@@ -0,0 +1,124 @@
|
|
|
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
|
+
/** 会话归属未知时的**显式占位**(H2③,2026-09-30;原定义在 src/hooks.ts,
|
|
27
|
+
* B 治本批迁入本单点——注释要点保真搬迁)。
|
|
28
|
+
*
|
|
29
|
+
* ⚠️ 不可退回「不传 session 键」:md_cg 的 `Principal.__init__` 在 session 为假值时
|
|
30
|
+
* 生成**进程级随机** `sess_<hex>`(md_cg/security.py:117)——插件不传,等于让一个
|
|
31
|
+
* 进程内所有「宿主未给标识」的会话共用一个**不可辨认**的随机桶:归属在审计上既
|
|
32
|
+
* 读不出是谁、跨进程也对不上,是静默的归属丢失。
|
|
33
|
+
* 本常量把这一态写成**显式值**:跨进程一致、可辨认、可审计,且不是伪造的宿主
|
|
34
|
+
* 会话 id(非 DSH 形态,服务端 `_normalize_session` 原样采用、不会被改写成别的桶)。
|
|
35
|
+
* 要读这个桶:`stg(op=timeline, session="unassigned")`。 */
|
|
36
|
+
export const UNASSIGNED_SESSION = 'unassigned';
|
|
37
|
+
/** 最近一次观测到的宿主会话标识(空串 = 未观测到会话)。
|
|
38
|
+
*
|
|
39
|
+
* ⚠️ 本状态是**模块级(进程级)单点**:hooks 面观测写入(src/hooks.ts 的
|
|
40
|
+
* session/event),工具面读它做写归因注入(src/tools.ts 的转发面)——两处必须
|
|
41
|
+
* 同源,否则注入的会话与写入的归属对不上(B 治本批的目的即在此)。
|
|
42
|
+
*
|
|
43
|
+
* 「本实例是否观测过」的**实例级门不在本模块**,在 src/hooks.ts 的
|
|
44
|
+
* installMemoryHooks 内(局部 `observedSession`):即「本实例观测到会话之后」
|
|
45
|
+
* 才用本单点值回落召回过滤。这是**经 owner 裁定的契约字面偏离(dwfq-7b3a555e-1)**
|
|
46
|
+
* ——契约建议的形态是 hooks 侧字面 `|| currentSession()`,但字面形态与硬边界
|
|
47
|
+
* 「test/session-attribution.test.ts 逐字未动且全绿」互斥(该守卫 ② 以
|
|
48
|
+
* 「新建 harness = 未观测」为前提,字面回落会读成前一实例的 sess_B)。保留
|
|
49
|
+
* 实例门的两条理由:
|
|
50
|
+
* ① 「新实例 = 干净状态」是原闭包变量 `lastSession` 的**有意属性**(新装的钩子
|
|
51
|
+
* 在观测到会话前,自动召回不加过滤)——实例隔离语义在测试面被真实保留;
|
|
52
|
+
* ② 与既有冻结守卫 ② 相容(守卫逐字未动且全绿是本批硬边界)。
|
|
53
|
+
* 真机单实例下两者**逐位等价**:唯一差异窗口是「本实例未观测 ∧ 模块单点非空」,
|
|
54
|
+
* 而模块值只由本实例的 hooks 观测写入,单实例下该窗口不可达;HMR 重载场景下新
|
|
55
|
+
* 实例回落 '' 而非上一会话值,属更保守的防御行为(已由 owner 裁定接受)。 */
|
|
56
|
+
let lastSession = '';
|
|
57
|
+
/** 记录一次会话观测(H2③ 观测面单点)。
|
|
58
|
+
*
|
|
59
|
+
* trim 后非空才写入——空串/纯空白**不覆盖**旧值(否则「宿主给了一次空标识」
|
|
60
|
+
* 会把已观测到的真会话抹掉,后续注入退化为 unassigned)。 */
|
|
61
|
+
export function noteSession(sid) {
|
|
62
|
+
const s = sid.trim();
|
|
63
|
+
if (s)
|
|
64
|
+
lastSession = s;
|
|
65
|
+
}
|
|
66
|
+
/** 当前运行期会话标识(空串 = 未观测到;调用方按 `|| UNASSIGNED_SESSION` 兜底)。 */
|
|
67
|
+
export function currentSession() {
|
|
68
|
+
return lastSession;
|
|
69
|
+
}
|
|
70
|
+
/** 写归因面判据:本次调用是否属于「要注入运行期会话」的写归因调用。
|
|
71
|
+
*
|
|
72
|
+
* **恰两条命中路径**(不得放宽、不得收窄出契约外):
|
|
73
|
+
* ① `mdcg_remember` —— md_cg 细粒度写入口(插件侧自动记忆/落图的写通道);
|
|
74
|
+
* ② `cg` 且 `op === 'write'` —— 认知图基元的带审核写路径。
|
|
75
|
+
*
|
|
76
|
+
* 为什么**只**这两个面(判据出处:md_cg/mcp_server.py):
|
|
77
|
+
* · **读面一律不注入**——`mdcg_recall` / `mdcg_search` / `mdcg_get` / `stg`
|
|
78
|
+
* 以及 `cg` 的其它 op,其 `session` 是**视图过滤**:服务端 schema 描述原文
|
|
79
|
+
* 「会话归属过滤(frontmatter.session;…缺省不过滤)」
|
|
80
|
+
* (md_cg/mcp_server.py:218-219 / :251-252 / :908-912 / :1074-1077)。
|
|
81
|
+
* 注入会把文档化的「缺省跨会话」翻转成「本会话视图」——那是功能收窄,
|
|
82
|
+
* 不是本批目标(读面要跨会话视图请显式 `stg(op=timeline, session="*")`)。
|
|
83
|
+
* · **op 特化语义不动**——`cg` 的 `sustain`/`session` 等 op 的 `session` 是
|
|
84
|
+
* 特化语义(resume/note 的**目标会话**),注入即污染其目标参数。
|
|
85
|
+
* · 归因与授权正交(issue #35 定稿):`call_tool` 的请求级 session **只做
|
|
86
|
+
* 归因**(写入归属/`_attribution` 取它),**不**改 `principal.session`
|
|
87
|
+
* (md_cg/mcp_server.py:3261-3264)——绑定档(private/secret)的读授权
|
|
88
|
+
* 锚定连接级身份,调用方自报的会话不构成看他人 private 的授权。
|
|
89
|
+
* 绑定档可见性判定 `MdCGSecure._readable`(md_cg/mdcos.py:5009-5053)
|
|
90
|
+
* 的会话绑定分支恒用 `nsess == self.principal.session`(:5051-5052),
|
|
91
|
+
* can_admin(设计者)豁免(:5045)——故本注入对绑定档无回归。
|
|
92
|
+
*
|
|
93
|
+
* ⚠️ 禁止把本判据放宽成「所有带 session 参数的工具」:那会把上面两类语义
|
|
94
|
+
* (读面视图过滤 / op 特化目标)一并改写,属越权改契约。
|
|
95
|
+
*/
|
|
96
|
+
function isWriteAttributionCall(toolName, args) {
|
|
97
|
+
if (toolName === 'mdcg_remember')
|
|
98
|
+
return true;
|
|
99
|
+
return toolName === 'cg' && args['op'] === 'write';
|
|
100
|
+
}
|
|
101
|
+
/** 请求是否已显式声明会话(string 且 trim 后非空 → 保留调用方声明,绝不覆盖)。 */
|
|
102
|
+
function hasDeclaredSession(args) {
|
|
103
|
+
const v = args['session'];
|
|
104
|
+
return typeof v === 'string' && v.trim() !== '';
|
|
105
|
+
}
|
|
106
|
+
/** 把**运行期会话**注入**写归因调用**(args 的 session 键不可用时才注入)。
|
|
107
|
+
*
|
|
108
|
+
* 注入条件:`session` 键缺失 / 非 string 类型 / trim 后空串 ⇒ 注入
|
|
109
|
+
* (`null`/`''`/空白一律视为「未声明」——服务端 `_declared_session` 对假值
|
|
110
|
+
* 同样解析为无归属声明,原样转发只会落回进程级随机 sess_* 兜底桶);
|
|
111
|
+
* 显式非空声明一律**不覆盖**(调用方自报优先——插件不替调用方改归属)。
|
|
112
|
+
*
|
|
113
|
+
* 注入值:`currentSession() || UNASSIGNED_SESSION`(未观测到会话时用
|
|
114
|
+
* 'unassigned' 显式占位——与 hooks 面 H2③ 同口径)。
|
|
115
|
+
*
|
|
116
|
+
* 命中且需注入 → 返回**新对象** `{ ...args, session: v }`(绝不 mutate 输入);
|
|
117
|
+
* 否则**原样返回**(同一引用)——未命中的调用零开销、零可观察差异。 */
|
|
118
|
+
export function attributeSession(toolName, args) {
|
|
119
|
+
if (!isWriteAttributionCall(toolName, args))
|
|
120
|
+
return args;
|
|
121
|
+
if (hasDeclaredSession(args))
|
|
122
|
+
return args;
|
|
123
|
+
return { ...args, session: currentSession() || UNASSIGNED_SESSION };
|
|
124
|
+
}
|
package/lib/tools.js
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
* 动态拉取意味着灵枢库升级新增工具后,DSH 侧零改动即可获得新能力。
|
|
6
6
|
*/
|
|
7
7
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
8
|
+
// B 治本批(2026-10-06):写归因注入单点(运行期会话 → 写归因调用)。
|
|
9
|
+
// 运行时导入写 `.js`(本仓口径:type-only 才写 `.ts`,tsconfig 未开
|
|
10
|
+
// allowImportingTsExtensions —— 值导入写 `.ts` 会 TS5097 编译失败)。
|
|
11
|
+
import { attributeSession } from './lib/session_state.js';
|
|
8
12
|
/** 默认暴露的核心工具集合:**记忆面已基元化**,只注册 `cg` / `stg` 两个认知基元。
|
|
9
13
|
*
|
|
10
14
|
* 三层拆分 S4 收敛后,插件工具面真源是 `md_cg.mcp_server`(不再是 pip aeis);
|
|
@@ -173,7 +177,14 @@ export async function registerLingshuTools(ctx, bridge, opts) {
|
|
|
173
177
|
async execute(args, exec) {
|
|
174
178
|
if (exec.signal.aborted)
|
|
175
179
|
throw new Error(`灵枢 ${tool.name} 已取消`);
|
|
176
|
-
|
|
180
|
+
// B 治本批(2026-10-06):agent 直调工具的转发面把**运行期会话**注入
|
|
181
|
+
// **写归因调用**——env(MDCG_SESSION)取消后,请求声明即归因唯一来源
|
|
182
|
+
// (md_cg/mcp_server.py 的 _declared_session:env > 请求声明 > 进程身份)。
|
|
183
|
+
// 判据单点在 lib/session_state.ts 的 attributeSession:只注入
|
|
184
|
+
// mdcg_remember 与 cg(op=write) 两个写面;读面(视图过滤)/ op 特化语义
|
|
185
|
+
// 一律不动。命中且未显式声明时返回新对象,否则原样透传。
|
|
186
|
+
const forwarded = attributeSession(tool.name, args);
|
|
187
|
+
const result = await bridge.callTool(tool.name, forwarded, exec.signal);
|
|
177
188
|
if (exec.signal.aborted)
|
|
178
189
|
throw new Error(`灵枢 ${tool.name} 已取消`);
|
|
179
190
|
if (result.isError) {
|
package/md_cg/audit.py
CHANGED
|
@@ -456,11 +456,17 @@ def _verify_code(payload, ctx):
|
|
|
456
456
|
# 被测命令只要输出非 gbk 字节,读取线程就抛 UnicodeDecodeError →
|
|
457
457
|
# p.stdout/p.stderr 可能为空 → 下一行的失败证据丢失,
|
|
458
458
|
# 「实测失败」会退化成一句没有依据的 REJECT(对齐 whitebox.py 的写法)。
|
|
459
|
+
# stdin=subprocess.DEVNULL(issue #63 同批加固):不指定 stdin 时子进程
|
|
460
|
+
# 继承父进程 stdin(常驻宿主下是 JSON-RPC 活管道),读 stdin 的测试
|
|
461
|
+
# 命令会悬挂到 timeout 才被 kill——形态对齐 `md_cg/run_tests.py:105`。
|
|
459
462
|
p = subprocess.run(argv, cwd=ctx.get("cwd"), capture_output=True,
|
|
460
463
|
text=True, encoding="utf-8", errors="replace",
|
|
461
|
-
shell=False,
|
|
464
|
+
shell=False, stdin=subprocess.DEVNULL,
|
|
462
465
|
timeout=int(os.environ.get("MDCG_CODE_TEST_TIMEOUT", "60")))
|
|
463
466
|
except (OSError, subprocess.SubprocessError) as exc:
|
|
467
|
+
# TimeoutExpired(SubprocessError 子类)也走这里:DEFER + 文案带异常
|
|
468
|
+
# 名与原文(如 "TimeoutExpired: Command '…' timed out after 60 seconds")
|
|
469
|
+
# ——超时**不吞**(issue #63)。
|
|
464
470
|
return _verdict(DEFER, "code", f"测试无法执行:{type(exc).__name__}: {exc}")
|
|
465
471
|
if p.returncode == 0:
|
|
466
472
|
return _verdict(ACCEPT, "code", f"实测通过:{cmd}")
|
|
@@ -315,9 +315,12 @@ def run(root=None, keep=False):
|
|
|
315
315
|
|
|
316
316
|
def _git_head():
|
|
317
317
|
try:
|
|
318
|
+
# timeout=10 既有;stdin=subprocess.DEVNULL 为 issue #63 同批加固——
|
|
319
|
+
# 不指定 stdin 时子进程继承父进程 stdin(常驻宿主下是 JSON-RPC 活管道)。
|
|
318
320
|
out = subprocess.run(["git", "rev-parse", "--short", "HEAD"],
|
|
319
321
|
capture_output=True, text=True, encoding="utf-8",
|
|
320
|
-
errors="replace", shell=False, timeout=10
|
|
322
|
+
errors="replace", shell=False, timeout=10,
|
|
323
|
+
stdin=subprocess.DEVNULL)
|
|
321
324
|
return (out.stdout or "").strip() or "unknown"
|
|
322
325
|
except Exception: # noqa: BLE001
|
|
323
326
|
return "unknown"
|
package/md_cg/bench_locomo_zh.py
CHANGED
|
@@ -303,6 +303,9 @@ ROW_RE = re.compile(
|
|
|
303
303
|
r"([\d.]+)%\s+([\d.]+)%\s+([\d.]+)\s*$", re.M)
|
|
304
304
|
GATE_RE = re.compile(r"拒答率:([\d.]+)%")
|
|
305
305
|
LINE_RE = re.compile(r"拒答线(正例 hit@1 题 Top-1 分 p10):([\d.]+)")
|
|
306
|
+
#: 单臂评测子进程的超时(秒)——issue #63 同批加固:评测跑批比 git 长,按
|
|
307
|
+
#: 「一臂为分钟级」给 1800s(30min)为安全上界;超时即 kill(不再是无限等待)。
|
|
308
|
+
_EVAL_TIMEOUT_S = 1800
|
|
306
309
|
|
|
307
310
|
|
|
308
311
|
# 生效条件:RUST_BIN 经 os.path.exists 为假时抛 SystemExit;否则以 argv 列表 [RUST_BIN, "--dataset", "lc", "--tag", name, "--lib", lib, "--qfile", qfile](extra 为真值时追加其元素)执行 subprocess.run,返回码非 0 或 stdout 未匹配 ROW_RE 时抛 SystemExit,成功时返回 (got, neg, line),其中 GATE_RE/LINE_RE 未命中时对应值为 None。
|
|
@@ -320,8 +323,15 @@ def run_one(name, lib, qfile, extra=None):
|
|
|
320
323
|
if extra:
|
|
321
324
|
argv += list(extra)
|
|
322
325
|
env = dict(os.environ, PYTHONUTF8="1")
|
|
323
|
-
|
|
324
|
-
|
|
326
|
+
try:
|
|
327
|
+
# timeout + stdin=DEVNULL(issue #63 同批加固):评测跑批有界化,
|
|
328
|
+
# 且不继承父进程 stdin(常驻宿主下是 JSON-RPC 活管道)。
|
|
329
|
+
p = subprocess.run(argv, capture_output=True, text=True,
|
|
330
|
+
encoding="utf-8", errors="replace", env=env, cwd=HERE,
|
|
331
|
+
timeout=_EVAL_TIMEOUT_S,
|
|
332
|
+
stdin=subprocess.DEVNULL)
|
|
333
|
+
except subprocess.TimeoutExpired:
|
|
334
|
+
raise SystemExit(f"[失败] {name}:评测超时(>{_EVAL_TIMEOUT_S}s)已被 kill")
|
|
325
335
|
if p.returncode != 0:
|
|
326
336
|
raise SystemExit(f"[失败] {name}:{(p.stderr or '')[-900:]}")
|
|
327
337
|
got = {}
|
package/md_cg/bench_zh_mad.py
CHANGED
|
@@ -481,6 +481,9 @@ ROW_RE = re.compile(
|
|
|
481
481
|
r"^\s*(precise|temporal|interference|reference)\s+(\d+)\s+"
|
|
482
482
|
r"([\d.]+)%\s+([\d.]+)%\s+([\d.]+)\s*$", re.M)
|
|
483
483
|
GROUPS = ["precise", "temporal", "interference", "reference"]
|
|
484
|
+
#: 单臂评测子进程的超时(秒)——issue #63 同批加固:评测跑批比 git 长,按
|
|
485
|
+
#: 「一臂为分钟级」给 1800s(30min)为安全上界;超时即 kill(不再是无限等待)。
|
|
486
|
+
_EVAL_TIMEOUT_S = 1800
|
|
484
487
|
|
|
485
488
|
|
|
486
489
|
# 生效条件:os.path.exists(RUST_BIN) 为真时以 argv=[RUST_BIN,"--dataset","mad","--tag",name,"--lib",lib](extra 为真值时追加 list(extra))执行 subprocess,返回码非 0 或 ROW_RE 在 stdout 未匹配到任何组时 raise SystemExit,否则返回 {组:(n,hit@1,hit@5,MRR)}。
|
|
@@ -498,8 +501,15 @@ def run_one(name, lib, extra=None):
|
|
|
498
501
|
if extra:
|
|
499
502
|
argv += list(extra)
|
|
500
503
|
env = dict(os.environ, PYTHONUTF8="1")
|
|
501
|
-
|
|
502
|
-
|
|
504
|
+
try:
|
|
505
|
+
# timeout + stdin=DEVNULL(issue #63 同批加固):评测跑批有界化,
|
|
506
|
+
# 且不继承父进程 stdin(常驻宿主下是 JSON-RPC 活管道)。
|
|
507
|
+
p = subprocess.run(argv, capture_output=True, text=True,
|
|
508
|
+
encoding="utf-8", errors="replace", env=env, cwd=HERE,
|
|
509
|
+
timeout=_EVAL_TIMEOUT_S,
|
|
510
|
+
stdin=subprocess.DEVNULL)
|
|
511
|
+
except subprocess.TimeoutExpired:
|
|
512
|
+
raise SystemExit(f"[失败] {name}:评测超时(>{_EVAL_TIMEOUT_S}s)已被 kill")
|
|
503
513
|
if p.returncode != 0:
|
|
504
514
|
raise SystemExit(f"[失败] {name}:{(p.stderr or '')[-900:]}")
|
|
505
515
|
got = {}
|
package/md_cg/interop.py
CHANGED
|
@@ -47,6 +47,11 @@ _FORBIDDEN_RES = [
|
|
|
47
47
|
]
|
|
48
48
|
_FORBIDDEN_KEYS = {"prompt", "content", "spec", "api_key", "base", "model",
|
|
49
49
|
"system_prompt", "user_prompt", "content_head"}
|
|
50
|
+
#: git add/commit 子进程的超时(秒)——issue #63 同批加固:本机 git 为秒级
|
|
51
|
+
#: 操作,120s 为安全上界;超时即 kill。**stdin=subprocess.DEVNULL 同加**:
|
|
52
|
+
#: 不指定 stdin 时子进程继承父进程 stdin(常驻宿主下是 JSON-RPC 活管道),
|
|
53
|
+
#: 读 stdin 的子进程会悬挂(先例形态 `md_cg/run_tests.py:105`)。
|
|
54
|
+
_GIT_TIMEOUT_S = 120
|
|
50
55
|
|
|
51
56
|
|
|
52
57
|
class InteropSanityError(ValueError):
|
|
@@ -323,15 +328,26 @@ def write_verdict_to_repo(verdict: dict, repo: str = HERE,
|
|
|
323
328
|
out = {"ok": True, "path": fp}
|
|
324
329
|
if do_commit:
|
|
325
330
|
import subprocess
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
331
|
+
try:
|
|
332
|
+
r1 = subprocess.run(["git", "add", os.path.relpath(fp, repo)],
|
|
333
|
+
cwd=repo, capture_output=True, text=True,
|
|
334
|
+
encoding="utf-8", errors="replace",
|
|
335
|
+
timeout=_GIT_TIMEOUT_S,
|
|
336
|
+
stdin=subprocess.DEVNULL)
|
|
337
|
+
r2 = subprocess.run(
|
|
338
|
+
["git", "commit", "-q", "-m",
|
|
339
|
+
f"interop(verifier): iter={it} verdict={verdict.get('verdict')} "
|
|
340
|
+
f"passed={verdict.get('passed')} failed={verdict.get('failed')}"],
|
|
341
|
+
cwd=repo, capture_output=True, text=True,
|
|
342
|
+
encoding="utf-8", errors="replace",
|
|
343
|
+
timeout=_GIT_TIMEOUT_S, stdin=subprocess.DEVNULL)
|
|
344
|
+
except subprocess.TimeoutExpired:
|
|
345
|
+
# 超时**不吞**:如实记进既有错误面(与 rc≠0 同一条 commit_err),
|
|
346
|
+
# 不假装提交成功(issue #63)。
|
|
347
|
+
out["committed"] = False
|
|
348
|
+
out["commit_err"] = ("git 超时(%ss)已被 kill:%s" %
|
|
349
|
+
(_GIT_TIMEOUT_S, out_dir))
|
|
350
|
+
return out
|
|
335
351
|
out["committed"] = r2.returncode == 0
|
|
336
352
|
if r2.returncode != 0:
|
|
337
353
|
out["commit_err"] = (r2.stderr or r1.stderr)[:200]
|
package/md_cg/lifecycle.py
CHANGED
|
@@ -103,6 +103,44 @@ def state_of(fm) -> str:
|
|
|
103
103
|
return s if s in STATES else "active"
|
|
104
104
|
|
|
105
105
|
|
|
106
|
+
# 生效条件:入参 e 为索引条目(或同形 frontmatter 副本)时返回 state_of(e) == "archived" 的布尔结果;缺 STATE_FIELD 或取值不在 STATES 时 state_of 回落 "active" ⇒ 返回 False。
|
|
107
|
+
def is_archived(e) -> bool:
|
|
108
|
+
"""默认检索剔除面=本函数(判据单点)。
|
|
109
|
+
|
|
110
|
+
`archived` 的语义(模块 docstring :42)是「不再参与**默认检索**」——本函数是该
|
|
111
|
+
宣称的**唯一判据**,全部消费点共同引用(判据单点,多个消费点):
|
|
112
|
+
|
|
113
|
+
· cg 检索面:`MdCGOS._candidates`(mdcos.py)——`MdCGSecure._candidates` 经
|
|
114
|
+
`super()` 覆盖叠加读可见性,故全部**检索类** cg 读 op(search / search_rrf /
|
|
115
|
+
recall / route…)都经它;
|
|
116
|
+
· stg 扫描面:`stg._scan_one`(stg.py,`_scan` 的逐条实现单点)——timeline /
|
|
117
|
+
anchors / consistency 三 op 的候选面同点覆盖(`relation` 面是显式 id 直读,
|
|
118
|
+
`state_chain` 走 append-only 台账,两者不经扫描面);
|
|
119
|
+
· 会话续接注入面:`MdCGOS.session_recall`(mdcos.py)的四个取数段——会话要点
|
|
120
|
+
`_session_notes`、活跃目标 `goals`、任务台账 `tasks`、未解问题 `unresolved`
|
|
121
|
+
(`goals`/`tasks` 经 `_retired_by_id` 按 id 回查索引后交本判据)。该 op 是
|
|
122
|
+
「装配续接包注入上下文/回给调用方」的注入面,与检索面同属「退役即不再默认
|
|
123
|
+
回出」;它自持取数面(不经 `_candidates`),故单点接线(独立复核 2026-10-06
|
|
124
|
+
两轮实测:接线前 archived 要点摘要、未解问题正文、**活跃目标与任务卡**均经
|
|
125
|
+
该 op 照旧回出;`recent` 段是事件窗口、`self_state` 是只读快照卡,不含节点
|
|
126
|
+
条目,不涉退役)。
|
|
127
|
+
|
|
128
|
+
**fail-open**(缺键/非法值 → active):存量节点不回填也照常参与检索
|
|
129
|
+
(存量零迁移、不误剔);「不误剔」的另一半是**只剔 archived**——
|
|
130
|
+
converged / demoted 是**降权轴**,仍参与默认检索。
|
|
131
|
+
|
|
132
|
+
不适用条件(**不**消费本函数,故 archived 期间照常可达):显式 id 直读
|
|
133
|
+
(`cg.get`)、审计面(`op=audit`)、工程台账的**显式管理查询面**
|
|
134
|
+
(`cg(op=goal, action=list)` / `cg(op=task, action=list)`;注意与消费面之别——
|
|
135
|
+
会话续接注入包内的对应段已按上表剔除,两者不冲突)、负记忆层
|
|
136
|
+
(rejected / unresolved 的覆盖标记面)、统计面(health / whoami / conformance)
|
|
137
|
+
——「退役不删除、可显式恢复」的承诺要求它们在 archived 期间照常可达。
|
|
138
|
+
边界(已知未覆盖面,另立裁定):基类 `MdCG.search`、`chain.adjacency` 拓扑面、
|
|
139
|
+
`subgraph.children_index/parents_index` 结构面、rust 检索内核。
|
|
140
|
+
"""
|
|
141
|
+
return state_of(e) == "archived"
|
|
142
|
+
|
|
143
|
+
|
|
106
144
|
# 生效条件:按 _RANK.get(dst, 0) > _RANK.get(src, 0) 判定,src 或 dst 不在 _RANK 键中时该侧按 0 参与比较。
|
|
107
145
|
def is_downgrade(src: str, dst: str) -> bool:
|
|
108
146
|
"""是否向「更低」的状态迁移(active < converged < demoted < archived)。"""
|