@furongjun1999/dsh-memory 0.7.3 → 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 +16 -15
- 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//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/lib/hooks.js +40 -18
- 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/lifecycle.py +38 -0
- package/md_cg/mcp_server.py +64 -11
- package/md_cg/mdcos.py +78 -9
- package/md_cg/state_slots.py +221 -0
- package/md_cg/stg.py +68 -3
- 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/tokens.py +8 -1
- package/package.json +1 -1
- package/skills/plugin.json +1 -1
- package/src/hooks.ts +40 -17
- package/src/lib/session_state.ts +127 -0
- package/src/tools.ts +12 -1
|
@@ -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/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)。"""
|
package/md_cg/mcp_server.py
CHANGED
|
@@ -727,12 +727,16 @@ KERNEL_TOOLS = [
|
|
|
727
727
|
"(offset/limit)、**分页前全集**聚合(aggregation=by_relation|"
|
|
728
728
|
"by_parent|by_child)、端点索引摘要(expand_nodes=true)。"
|
|
729
729
|
"时间条件缺省走**观察轴**(派生边只有写入时刻 t,无效力声明)"
|
|
730
|
-
"——与 op=read 缺省效力轴**有意不同**,由库层单点校验。"
|
|
730
|
+
"——与 op=read 缺省效力轴**有意不同**,由库层单点校验。"
|
|
731
|
+
"op=state_event:状态事件记账(追加一条五元事件"
|
|
732
|
+
"subject/slot/old/new/kind;actor 恒取令牌;"
|
|
733
|
+
"查询走 stg(op=state_chain))。",
|
|
731
734
|
"inputSchema": _s("",
|
|
732
735
|
op=_p("string", "route|read|write|verify|review|protect|identity|"
|
|
733
736
|
"consistency|metacognition|self_state|evolution|sustain|"
|
|
734
737
|
"scrub|predict|causal|"
|
|
735
|
-
"forget|goal|task|recent|info|index_code|
|
|
738
|
+
"forget|goal|task|recent|state_event|info|index_code|"
|
|
739
|
+
"index_doc|ref|whitebox|"
|
|
736
740
|
"theory|link|session|ingest|export|maintain|consolidate|"
|
|
737
741
|
"insight|ccg|status|edges|audit|help", True),
|
|
738
742
|
ccg=_p("object", "CCG 六要素编译器入参:{action, node_id, dialog, marks, "
|
|
@@ -752,6 +756,10 @@ KERNEL_TOOLS = [
|
|
|
752
756
|
"write 时作为来源证据(user=外部惊奇)"),
|
|
753
757
|
meta=_p("object", "recent op:附加元数据"),
|
|
754
758
|
window=_p("integer", "recent op:滚动窗口大小(默认 200)"),
|
|
759
|
+
slot=_p("string", "state_event:事件槽位(五元之一——哪项状态)"),
|
|
760
|
+
old=_p("string", "state_event:变更前值(撤回时留空、new 必填;"
|
|
761
|
+
"old/new 双空非法)"),
|
|
762
|
+
new=_p("string", "state_event:变更后值(撤回=留空;空值即值作废)"),
|
|
755
763
|
include_recent=_p("boolean", "read:是否附近期事件窗口(默认否)"),
|
|
756
764
|
limit=_p("integer", "goal/recent 的返回条数;read 的近期事件条数;"
|
|
757
765
|
"edges 分页条数(缺省 50、上限 500,0/负数报错不当「全部」)"),
|
|
@@ -820,7 +828,8 @@ KERNEL_TOOLS = [
|
|
|
820
828
|
"receipt(回执审计:命令与预期输出);"
|
|
821
829
|
"非法值库层报错(fail-closed)"),
|
|
822
830
|
budget_tokens=_p("integer", "read 的 token 预算"),
|
|
823
|
-
evidence=_p("string", "verify
|
|
831
|
+
evidence=_p("string", "verify 的证据;state_event:出处(五元之一,"
|
|
832
|
+
"回指原文/轮次)"),
|
|
824
833
|
verdict=_p("string", "verify 裁决:confirmed|weakened|falsified;"
|
|
825
834
|
"insight verify:verified|falsified"),
|
|
826
835
|
question=_p("string", "whitebox:ask/verify 的问题"),
|
|
@@ -913,7 +922,8 @@ KERNEL_TOOLS = [
|
|
|
913
922
|
"节点;**未生效(valid_from 未到)保留**"
|
|
914
923
|
"(两者语义相反,预约/计划类记忆生效前仍可召回)"),
|
|
915
924
|
ts=_p("number", "sustain note:事件时间戳"),
|
|
916
|
-
seq=_p("integer", "sustain note:事件序号"
|
|
925
|
+
seq=_p("integer", "sustain note:事件序号;state_event:事件序号"
|
|
926
|
+
"(五元之一——轮或序号)"),
|
|
917
927
|
task_running=_p("boolean", "sustain:任务执行中(心跳阈值放宽)"),
|
|
918
928
|
beat_interval=_p("number", "sustain start:心跳间隔秒(默认 600)"),
|
|
919
929
|
heal_interval=_p("number", "sustain start:巡检间隔秒(默认 300)"),
|
|
@@ -968,7 +978,8 @@ KERNEL_TOOLS = [
|
|
|
968
978
|
set=_p("object", "link policy:设置子系统签名策略"),
|
|
969
979
|
signers_file=_p("string", "link:签名策略文件(缺省 ~/.mdcg/_signers.json)"),
|
|
970
980
|
swarm=_p("string", "link:跨节点共享目录(缺省 ~/.mdcg/swarm)"),
|
|
971
|
-
subject=_p("string", "link evidence:按主体过滤(如 agent:node-x
|
|
981
|
+
subject=_p("string", "link evidence:按主体过滤(如 agent:node-x);"
|
|
982
|
+
"state_event:事件主体(五元之一——谁的状态)"),
|
|
972
983
|
subjects=_p("array", "link export:限定导出的主体列表"),
|
|
973
984
|
out=_p("string", "link export:证据包输出路径;export/maintain 输出路径"),
|
|
974
985
|
pack=_p("object", "link import:内联证据包(与 path 二选一)"),
|
|
@@ -1063,10 +1074,12 @@ KERNEL_TOOLS = [
|
|
|
1063
1074
|
"op=timeline:按时间排序(可用 session 切本会话/跨会话视图,"
|
|
1064
1075
|
"看「所有会话做了什么」传 session=\"*\",返回项带会话归属);"
|
|
1065
1076
|
"op=anchors:落在时间窗/空间范围的节点;"
|
|
1066
|
-
"op=consistency
|
|
1077
|
+
"op=consistency:时空字段自洽性检查;"
|
|
1078
|
+
"op=state_chain:状态槽位投影(现值/区间/变迁史;"
|
|
1079
|
+
"flag MDCG_STG_STATE 默认关)。"
|
|
1067
1080
|
"四个 op 均可用 time_axis 切换时间轴(缺省 observed)。",
|
|
1068
1081
|
"inputSchema": _s("",
|
|
1069
|
-
op=_p("string", "relation|timeline|anchors|consistency", True),
|
|
1082
|
+
op=_p("string", "relation|timeline|anchors|consistency|state_chain", True),
|
|
1070
1083
|
a=_p("string", "relation 的节点 a"), b=_p("string", "relation 的节点 b"),
|
|
1071
1084
|
time_window=_p("array", "anchors 的时间窗 [t1,t2]"),
|
|
1072
1085
|
bbox=_p("array", "anchors 的包围盒 [x1,y1,x2,y2]"),
|
|
@@ -1077,7 +1090,12 @@ KERNEL_TOOLS = [
|
|
|
1077
1090
|
"(frontmatter.session 精确相等)"),
|
|
1078
1091
|
desc=_p("boolean", "timeline 是否倒序(默认是)"),
|
|
1079
1092
|
time_axis=_p("string", "时间轴:observed(观察轴 temporal/time_window,"
|
|
1080
|
-
"缺省)| effective(效力轴 effective_from/until)")
|
|
1093
|
+
"缺省)| effective(效力轴 effective_from/until)"),
|
|
1094
|
+
subject=_p("string", "state_chain 的主体(省略=不过滤)"),
|
|
1095
|
+
slot=_p("string", "state_chain 的槽位(省略=不过滤)"),
|
|
1096
|
+
include_retired=_p("boolean", "state_chain 是否含退役槽位(默认 false;"
|
|
1097
|
+
"退役不删除,只是默认不列)"),
|
|
1098
|
+
history=_p("boolean", "state_chain 是否带变迁史(默认 true)")),
|
|
1081
1099
|
},
|
|
1082
1100
|
]
|
|
1083
1101
|
|
|
@@ -2108,7 +2126,10 @@ def _cg_dispatch(cg, a):
|
|
|
2108
2126
|
return _help_call(cg, a)
|
|
2109
2127
|
_p = getattr(cg, "principal", None)
|
|
2110
2128
|
if _p is not None and hasattr(_p, "require_op"):
|
|
2111
|
-
|
|
2129
|
+
# P3 写口(state_event)映射到既有 "write" op 词——不给令牌 face 引入新
|
|
2130
|
+
# op 词(角色白名单/派生收窄面零改动):凡 ops 白名单含 write 的角色可
|
|
2131
|
+
# 记账,guest/只读令牌照拒。其余 op 逐位不变。
|
|
2132
|
+
_p.require_op("write" if op == "state_event" else op)
|
|
2112
2133
|
|
|
2113
2134
|
if op == "status":
|
|
2114
2135
|
return _status_call(cg, a)
|
|
@@ -2428,6 +2449,26 @@ def _cg_dispatch(cg, a):
|
|
|
2428
2449
|
return {"events": cg.recent_events(limit=int(a.get("limit") or 20),
|
|
2429
2450
|
roles=a.get("roles"))}
|
|
2430
2451
|
|
|
2452
|
+
if op == "state_event":
|
|
2453
|
+
# L2 状态事件记账(P3 写口;设计稿 §2「事件是源、槽位是投影、查询走 stg」)。
|
|
2454
|
+
# actor **恒取令牌**(不接受请求参数——沿本文件既有防伪造口径:客户端
|
|
2455
|
+
# 不得改写归属);台账是直写盘面的 append-only 文件、**绕过库层节点写闸**,
|
|
2456
|
+
# 故在此显式补齐 require_write(缺了它:ops 白名单放行但 can_write=False
|
|
2457
|
+
# 的令牌也能落盘)。kind 归一:空串归 None——「未分类」是合法记账,
|
|
2458
|
+
# 而 append 对空串抛 ValueError。append 的 ValueError(缺 subject/slot、
|
|
2459
|
+
# old/new 双空、非法 kind)原样上抛(fail-closed,不吞)。
|
|
2460
|
+
from . import state_events as _se
|
|
2461
|
+
_actor = _p.actor if _p is not None else "system"
|
|
2462
|
+
if _p is not None:
|
|
2463
|
+
_p.require_write("internal")
|
|
2464
|
+
rec = _se.append(cg, a.get("subject"), a.get("slot"),
|
|
2465
|
+
old=a.get("old"), new=a.get("new"),
|
|
2466
|
+
kind=(a.get("kind") or "").strip() or None,
|
|
2467
|
+
seq=a.get("seq"),
|
|
2468
|
+
evidence=a.get("evidence") or "",
|
|
2469
|
+
actor=_actor)
|
|
2470
|
+
return {"ok": True, "event": rec}
|
|
2471
|
+
|
|
2431
2472
|
if op == "verify":
|
|
2432
2473
|
return cg.verify(a.get("node_id", ""), a.get("evidence", ""),
|
|
2433
2474
|
a.get("verdict", ""),
|
|
@@ -3212,13 +3253,25 @@ def _stg_call(cg, a):
|
|
|
3212
3253
|
if op == "consistency":
|
|
3213
3254
|
return stg.consistency(cg, layer=a.get("layer"),
|
|
3214
3255
|
limit=int(a.get("limit") or 50), time_axis=axis)
|
|
3256
|
+
if op == "state_chain":
|
|
3257
|
+
# 第 5 op:状态槽位投影(flag MDCG_STG_STATE **默认关**)。关臂由 stg 侧
|
|
3258
|
+
# 返回 disabled 返回体(**不抛**——「没开」不是错误,是与「没命中」可分辨的
|
|
3259
|
+
# 如实答复);开臂委托 md_cg/state_slots.py 现算(本层不复制投影逻辑)。
|
|
3260
|
+
# time_axis 与本 op 无关:台账事件行只有落账时间戳 t,无第二轴(不伪造)。
|
|
3261
|
+
return stg.state_chain(
|
|
3262
|
+
cg, subject=a.get("subject"), slot=a.get("slot"),
|
|
3263
|
+
include_retired=bool(a.get("include_retired", False)),
|
|
3264
|
+
history=bool(a.get("history", True)),
|
|
3265
|
+
limit=int(a.get("limit") or 50))
|
|
3215
3266
|
if not op:
|
|
3216
3267
|
# fail-closed 且给出可操作提示:stg 的 op 四值签名区分度低于 cg(relation 需
|
|
3217
3268
|
# a+b、timeline/anchors/consistency 皆以 layer+limit 为主),**不做签名推导**
|
|
3218
3269
|
# ——猜错会静默返回错误视图,比报错更贵。
|
|
3219
3270
|
raise ValueError("stg 的 op 为必填参数(缺失即报错,不静默降级);"
|
|
3220
|
-
"允许 op:relation | timeline | anchors | consistency"
|
|
3221
|
-
|
|
3271
|
+
"允许 op:relation | timeline | anchors | consistency | "
|
|
3272
|
+
"state_chain")
|
|
3273
|
+
raise ValueError(f"stg 未知 op:{op}(允许 relation | timeline | anchors | "
|
|
3274
|
+
f"consistency | state_chain)")
|
|
3222
3275
|
|
|
3223
3276
|
|
|
3224
3277
|
# --------------------------------------------------------------------------
|
package/md_cg/mdcos.py
CHANGED
|
@@ -37,7 +37,7 @@ from .mdcg import (MdCG, expand_query_terms, bigrams, normalize_en, STATE_ACCEPT
|
|
|
37
37
|
from . import (nodefile, routing, chain, subgraph, forgetting, protect,
|
|
38
38
|
identity, consistency, metacognition, crypto, sustain,
|
|
39
39
|
self_state, predict, evolution, weights, pooling,
|
|
40
|
-
writelimit, reach, trust, roleviews, autonomy_modes)
|
|
40
|
+
writelimit, reach, trust, roleviews, autonomy_modes, lifecycle)
|
|
41
41
|
from .fsutil import (FileLock, atomic_write, append_jsonl, read_jsonl,
|
|
42
42
|
read_jsonl_tail, count_jsonl, publish)
|
|
43
43
|
from .security import (Principal, TenantRegistry, AccessDenied,
|
|
@@ -425,6 +425,26 @@ def _note_visible(cg, e) -> bool:
|
|
|
425
425
|
return _security.visible_to(cg, e)
|
|
426
426
|
|
|
427
427
|
|
|
428
|
+
# 生效条件:cg 索引的 nodes 中该 nid 有条目且 lifecycle.is_archived(该条目) 为真时返回 True;条目缺失时把 None 交给 state_of ⇒ 返回 False(fail-open,与判据单点同语义)。
|
|
429
|
+
def _retired_by_id(cg, nid) -> bool:
|
|
430
|
+
"""退役判据在「**按 id 取**的节点」上的单点包装(会话续接包台账段用)。
|
|
431
|
+
|
|
432
|
+
台账两段(goals / tasks)拿到的是 `_goal_entry` / `tasks._entry` 的**对外条目**
|
|
433
|
+
(只有 id,没有索引条目本身),故此处按 id 回查索引再交判据单点
|
|
434
|
+
`lifecycle.is_archived`——判据仍只此一份,不另立第二套口径。
|
|
435
|
+
"""
|
|
436
|
+
e = ((getattr(cg, "index", None) or {}).get("nodes") or {}).get(nid)
|
|
437
|
+
return lifecycle.is_archived(e)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
#: 会话续接包台账段(goals / tasks)的取数窗:取全量再过滤退役再截断
|
|
441
|
+
#: (`list_goals` 本就全表扫;`tasks.session_tasks` 内部本就先建全量列表再切片,
|
|
442
|
+
#: 故不增遍历成本),避免「窗口内的 archived 被剔后不足额」。
|
|
443
|
+
_LEDGER_WINDOW = 10 ** 6
|
|
444
|
+
#: 台账段每段最终回出的条数(与改动前的 limit=5 逐位一致)。
|
|
445
|
+
_PACK_ROWS = 5
|
|
446
|
+
|
|
447
|
+
|
|
428
448
|
# 生效条件:以任意 root 构造时按其拼接 audit_log/hippocampus/trash 等路径并 makedirs 创建 hippocampus 与 trash_dir(exist_ok=True),autoflush 透传父类、actor 存入 self.actor;
|
|
429
449
|
class MdCGOS(MdCG):
|
|
430
450
|
"""MdCG + 记忆 OS 七项能力。"""
|
|
@@ -747,7 +767,7 @@ class MdCGOS(MdCG):
|
|
|
747
767
|
payload_hash=_sig(content))
|
|
748
768
|
return nid
|
|
749
769
|
|
|
750
|
-
# 生效条件:在已用 root 构造的实例上遍历 index["nodes"],恒剔除 layer 为 rejected/unresolved/goals
|
|
770
|
+
# 生效条件:在已用 root 构造的实例上遍历 index["nodes"],恒剔除 layer 为 rejected/unresolved/goals 的节点,恒剔除 lifecycle.is_archived(e) 为真的节点(退役不参与默认检索;缺 lifecycle 键=active 照常,converged/demoted 仍参与),session 为真值而 e["session"] 不等于它时剔除,e["branch_id"] 不在 (None, branch) 时剔除(branch=None 时只留 branch_id 为 None 者),validity 为真值而 trust.is_expired(e) 为真时剔除(**只排已过期,not_yet 保留**),layer 为真值而 layer 不等时剔除,roles 不为 None 时仅留 role 落在 roles 内的节点;view 为真值时 role 维度裁决权移交 roleviews.matches(receipt=工作角色白名单须绕过默认剔除才可达,非法 view ValueError),view 为假值且 include_work 为假时剔除 WORK_ROLES 角色,时间算子启用时按 time_axis 轴过滤(效力轴不可判定 fail-open、观察轴不可判定 fail-closed 并入 self._time_filter_stat),其余收集进 out 返回。
|
|
751
771
|
def _candidates(self, layer=None, roles=None, include_work=False,
|
|
752
772
|
session=None, branch=None, validity=None,
|
|
753
773
|
start_time=None, end_time=None, start_operator=None,
|
|
@@ -782,6 +802,19 @@ class MdCGOS(MdCG):
|
|
|
782
802
|
if e.get("layer") in NEG_ROUTE_LAYERS:
|
|
783
803
|
continue # 负记忆走覆盖标记;目标只做定向,都不进正排
|
|
784
804
|
# (真源 mdcg.NEG_ROUTE_LAYERS,本处直接导入,不留第二份字面量)
|
|
805
|
+
if lifecycle.is_archived(e):
|
|
806
|
+
continue # lifecycle.py:42 标称兑现:archived 不参与默认检索。
|
|
807
|
+
# 本处是 **cg 检索面单点**——`MdCGSecure._candidates`
|
|
808
|
+
# 经 super() 在此之上叠加读可见性,故全部**检索类**
|
|
809
|
+
# cg 读 op(search / search_rrf / recall / route…)经此
|
|
810
|
+
# 一处。cg 的读 op 并非全走候选池:`op=session` 的续接包
|
|
811
|
+
# 自持取数面,另在 `_session_notes` / `session_recall`
|
|
812
|
+
# 接同一判据(见 lifecycle.is_archived 的消费面清单)。
|
|
813
|
+
# 判据单点在 lifecycle.is_archived(fail-open:缺键=
|
|
814
|
+
# active 照常;只剔 archived——converged/demoted 是
|
|
815
|
+
# **降权轴**,仍参与默认检索)。
|
|
816
|
+
# 直读面(cg.get / op=audit / 库层 set_state 恢复路径)**不**
|
|
817
|
+
# 经本函数:退役不删除、可显式恢复。见 lifecycle.is_archived
|
|
785
818
|
# '"*"' = 显式跨会话(读遍所有会话);缺省 None 同义(见 stg.timeline)
|
|
786
819
|
if session and session != "*" and e.get("session") != session:
|
|
787
820
|
continue
|
|
@@ -3153,11 +3186,18 @@ class MdCGOS(MdCG):
|
|
|
3153
3186
|
return {"ok": True, "id": nid, "session": session, "layer": layer,
|
|
3154
3187
|
"basis": basis, "tokens": est_tokens(content)}
|
|
3155
3188
|
|
|
3156
|
-
# 生效条件:当 session 传入且为真时仅保留 tags 含 f"session:{session}"
|
|
3189
|
+
# 生效条件:当 session 传入且为真时仅保留 tags 含 f"session:{session}" 的项;恒剔除 lifecycle.is_archived(e) 为真的条目(退役不参与默认注入,判据单点在 lifecycle.py);保留 tags 含 SESSION_TAG 或任一以 "session:" 开头的索引节点,且该索引条目经可见性单点 _readable 判为可见,_read 的 content 为 None 则跳过;按 created_at 降序后返回前 max(1, int(limit or 5)) 条,limit 为假值(含 0/None)按 5 处理;
|
|
3157
3190
|
def _session_notes(self, session=None, limit=5):
|
|
3158
3191
|
"""按时间倒序取会话要点(索引过滤 + 可见性闸 + 惰性回读摘要)。只读,不写盘。"""
|
|
3159
3192
|
out = []
|
|
3160
3193
|
for nid, e in list((self.index.get("nodes") or {}).items()):
|
|
3194
|
+
if lifecycle.is_archived(e):
|
|
3195
|
+
continue # 退役剔除(判据单点 lifecycle.is_archived,
|
|
3196
|
+
# 与 cg 检索面 `_candidates`、stg 面
|
|
3197
|
+
# `_scan_one` 同一份):会话续接包是「把节点
|
|
3198
|
+
# **内容摘要**回给调用方」的注入面,archived
|
|
3199
|
+
# 不再参与默认注入(与 §5.2 退役纪律同口径);
|
|
3200
|
+
# fail-open:缺键/非法值=active 照常。
|
|
3161
3201
|
tags = list(e.get("tags") or [])
|
|
3162
3202
|
if self.SESSION_TAG not in tags and not any(
|
|
3163
3203
|
str(t).startswith("session:") for t in tags):
|
|
@@ -3191,7 +3231,7 @@ class MdCGOS(MdCG):
|
|
|
3191
3231
|
out.sort(key=lambda n: (-n["created_at"], str(n.get("id") or "")))
|
|
3192
3232
|
return out[:max(1, int(limit or 5))]
|
|
3193
3233
|
|
|
3194
|
-
# 生效条件:limit 经 max(1,min(int(limit or 5),50))、budget_tokens 经 max(200,int(budget_tokens or 1200)) 归一后逐段取数(include_state 为真才取 self_state
|
|
3234
|
+
# 生效条件:limit 经 max(1,min(int(limit or 5),50))、budget_tokens 经 max(200,int(budget_tokens or 1200)) 归一后逐段取数(include_state 为真才取 self_state;会话要点段、未解问题段与台账两段(goals/tasks)恒剔除 lifecycle.is_archived 条目——退役不参与默认注入;台账段取全量窗过滤后再截断 `_PACK_ROWS` 条,`tasks.active_total/done_total` 取剔除后的全窗计数),每段异常只把段名追加进 degraded,再由 while 循环按预算交替裁 recent/notes 尾部、任务段最后才裁并置 tasks_truncated。
|
|
3195
3235
|
def session_recall(self, session=None, limit=5, recent_limit=10,
|
|
3196
3236
|
budget_tokens=1200, include_state=True):
|
|
3197
3237
|
"""按需恢复:一次调用返回「可续接的上下文包」(替代 hook 自动注入)。
|
|
@@ -3203,6 +3243,15 @@ class MdCGOS(MdCG):
|
|
|
3203
3243
|
任务段(2026-09-16 新增)是「忘记已实现的工程」的直接解药:新会话开机即见
|
|
3204
3244
|
「还在做的」与「刚做完的」,不必先想到去查。任务属结构层、跨会话稳定,
|
|
3205
3245
|
故**不按 session 过滤**——工程台账跟着工程走,不跟着会话走。
|
|
3246
|
+
|
|
3247
|
+
**退役口径(本包统一)**:本 op 是**注入面**(内容直接回给调用方/进上下文),
|
|
3248
|
+
故四个取数段(notes / goals / tasks[active/done 两清单]/ unresolved;合计
|
|
3249
|
+
五个清单)一律剔除
|
|
3250
|
+
`lifecycle_state=archived`(判据单点 `lifecycle.is_archived`)——「退役不参与
|
|
3251
|
+
默认注入」。`recent` 段是**事件窗口**(非节点)、`self_state` 是只读快照卡,
|
|
3252
|
+
两者不含节点条目,不涉及退役。**管理查询面不受限**:`cg(op=goal, action=list)`
|
|
3253
|
+
与 `cg(op=task, action=list)` 直读台账、archived 期间照常可达(两处不冲突:
|
|
3254
|
+
同一份台账,注入面剔除、管理面直读)。
|
|
3206
3255
|
"""
|
|
3207
3256
|
limit = max(1, min(int(limit or 5), 50))
|
|
3208
3257
|
budget = max(200, int(budget_tokens or 1200))
|
|
@@ -3218,26 +3267,41 @@ class MdCGOS(MdCG):
|
|
|
3218
3267
|
except Exception: # noqa: BLE001
|
|
3219
3268
|
pack["degraded"].append("notes")
|
|
3220
3269
|
# ② 活跃目标(检索定向的默认来源)
|
|
3270
|
+
# 退役剔除(判据单点 lifecycle.is_archived,经 `_retired_by_id` 按 id 取):
|
|
3271
|
+
# 续接包是**注入面**,archived 不参与默认注入(同 ① 与 ④)。取数窗取
|
|
3272
|
+
# **全量**(limit=None)再过滤再截断——否则窗口内的 archived 被剔后不足额,
|
|
3273
|
+
# 本该补位的在役目标补不上(`list_goals` 本就是全表扫,无额外成本)。
|
|
3274
|
+
# 管理查询面 `cg(op=goal, action=list)` **不受限**(不经本函数、直读台账)。
|
|
3221
3275
|
try:
|
|
3222
3276
|
pack["goals"] = [{"id": g["id"], "goal": g["goal"],
|
|
3223
3277
|
"priority": g["priority"]}
|
|
3224
|
-
for g in self.active_goals(limit=
|
|
3278
|
+
for g in self.active_goals(limit=None)
|
|
3279
|
+
if not _retired_by_id(self, g.get("id"))
|
|
3280
|
+
][:_PACK_ROWS]
|
|
3225
3281
|
except Exception: # noqa: BLE001
|
|
3226
3282
|
pack["degraded"].append("goals")
|
|
3227
3283
|
# ②.5 任务台账(structural 层)——见 docstring:不按 session 过滤
|
|
3284
|
+
# 退役剔除(同上,台账段是同一注入面的子段):列表剔 archived、`*_total`
|
|
3285
|
+
# 随之取**剔除后**的全窗计数(列表是它的前 `_PACK_ROWS` 条,两者同源自洽);
|
|
3286
|
+
# 管理查询面 `cg(op=task, action=list)` 仍直读台账、**不受限**。
|
|
3228
3287
|
try:
|
|
3229
3288
|
from . import tasks as _tasks
|
|
3230
|
-
ts = _tasks.session_tasks(self, active_limit=
|
|
3289
|
+
ts = _tasks.session_tasks(self, active_limit=_LEDGER_WINDOW,
|
|
3290
|
+
done_limit=_LEDGER_WINDOW)
|
|
3291
|
+
_act = [t for t in ts["active"]
|
|
3292
|
+
if not _retired_by_id(self, t.get("id"))]
|
|
3293
|
+
_done = [t for t in ts["done"]
|
|
3294
|
+
if not _retired_by_id(self, t.get("id"))]
|
|
3231
3295
|
pack["tasks"] = {
|
|
3232
3296
|
"active": [{"id": t["id"], "name": t["name"], "status": t["status"],
|
|
3233
3297
|
"plan": (t.get("plan") or "")[:300],
|
|
3234
3298
|
"updated_at": t.get("updated_at")}
|
|
3235
|
-
for t in
|
|
3299
|
+
for t in _act[:_PACK_ROWS]],
|
|
3236
3300
|
"done": [{"id": t["id"], "name": t["name"], "status": t["status"],
|
|
3237
3301
|
"result": (t.get("result") or "")[:300],
|
|
3238
3302
|
"updated_at": t.get("updated_at")}
|
|
3239
|
-
for t in
|
|
3240
|
-
"active_total":
|
|
3303
|
+
for t in _done[:_PACK_ROWS]],
|
|
3304
|
+
"active_total": len(_act), "done_total": len(_done)}
|
|
3241
3305
|
except Exception: # noqa: BLE001
|
|
3242
3306
|
pack["degraded"].append("tasks")
|
|
3243
3307
|
# ③ 近期事件(原始滚动窗口)
|
|
@@ -3253,6 +3317,11 @@ class MdCGOS(MdCG):
|
|
|
3253
3317
|
for nid, e in list((self.index.get("nodes") or {}).items()):
|
|
3254
3318
|
if e.get("layer") != "unresolved":
|
|
3255
3319
|
continue
|
|
3320
|
+
if lifecycle.is_archived(e):
|
|
3321
|
+
continue # 退役剔除(同 ① 会话要点:本条是续接包里
|
|
3322
|
+
# 「未解问题正文片段回给调用方」的第二处注入
|
|
3323
|
+
# 子面,判据仍是 lifecycle.is_archived 单点;
|
|
3324
|
+
# fail-open 同上)
|
|
3256
3325
|
# N211(2026-09-28,本族第二出口,原树内并号 N209 按 v24 裁定改判):与 ① 会话要点同根——此处也
|
|
3257
3326
|
# 是「只按 layer 过滤 → 回读正文 → 抽出 `# 问题:`」的直读出口,
|
|
3258
3327
|
# 无 _readable。实测:restricted 档未解问题的正文经 session_recall
|