@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
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""状态槽位寄存器投影(L2「主体×谓词槽位」)——**事件是源、槽位是投影**。
|
|
3
|
+
|
|
4
|
+
设计稿:`docs/plans/语义时空图补全_世界模型功能端_设计_v0.1.md` §2/§3(P2 批)。
|
|
5
|
+
|
|
6
|
+
本模块是**投影,不是第二真源**:
|
|
7
|
+
· 源=`md_cg/state_events.py` 的 append-only 台账 `<root>/_state_events.jsonl`;
|
|
8
|
+
· 查询时**现算**——不落盘、不建缓存、不写任何盘面文件;
|
|
9
|
+
· 台账追加一条,投影立刻随之变化(无缓存、无物化表 ⇒ 无第二真源、无漂移面);
|
|
10
|
+
· 本模块**只读**台账(经 `state_events.read`;`append` 归 `state_events`)。
|
|
11
|
+
|
|
12
|
+
投影单元字段(设计稿 §3 表逐字):
|
|
13
|
+
subject / slot / value / state(active|retired) / t_from / t_to /
|
|
14
|
+
seq_from / seq_to / tier / alternatives / blindspots / history / event_count
|
|
15
|
+
|
|
16
|
+
回放规则(分组键 `(subject, slot)`,组内按 `state_events.read()` 的**落盘序**):
|
|
17
|
+
· `value` 自 None 起:事件 `new` 非空 → `value=new`;`new` 空(撤回)→ `value=None`
|
|
18
|
+
且 `state=retired`;其后带值事件自然复活(事件序自然处理,无特判分支)。
|
|
19
|
+
· `state`:**最后一个事件的形态**决定——`new` 空 → `'retired'`;否则 `'active'`。
|
|
20
|
+
· 区间(取值一律用事件行的 `t` 落账时间戳,不另算时间):
|
|
21
|
+
`t_from`/`seq_from` =**最后产生当前值**的那条事件的 `t`/`seq`(retired 态=
|
|
22
|
+
最后一个**有值**事件的);`t_to`/`seq_to` =**覆盖它的下一条事件**的 `t`/`seq`
|
|
23
|
+
(不存在=None,即「未闭合/仍有效」);retired 态的覆盖者=撤回事件(闭合点
|
|
24
|
+
=值作废时刻)。
|
|
25
|
+
· `tier` =最后有值事件的顶层 `tier` 透传(`append(extra=…)` 会把 extra 并入行
|
|
26
|
+
顶层,故「extra.tier」与「行顶层 tier」是同一处);缺失(键不在或为 None)→
|
|
27
|
+
`TIER_UNSPECIFIED`——**不编造**。
|
|
28
|
+
· `alternatives` / `blindspots`:本批**结构在场、恒为空列表**——「同期未决值」与
|
|
29
|
+
「声明缺口」的判定口径**后置**(P1 事件行尚无该信号),**不得臆造填充**。
|
|
30
|
+
· `history` =该槽位全部事件原样(每条=事件行的 `t/seq/old/new/kind/evidence/
|
|
31
|
+
actor` 七字段,值不改写;缺键取 None);`history=False` 时**不带该键**。
|
|
32
|
+
|
|
33
|
+
撤回判据取 **payload**(`new` 为空)而不是 `kind` 标签:台账口径是「撤回=old 有值而
|
|
34
|
+
new 为空」(`state_events` 模块头),且该模块明言「分类本身不进判据(记账不判语义)」
|
|
35
|
+
——两者一致时等价;冲突时以 payload 为准(不按标签改写数据)。判据单点=`_is_void`。
|
|
36
|
+
|
|
37
|
+
过滤与排序:`subject`/`slot` 为 None 不加过滤(该过滤**委托** `state_events.read`,
|
|
38
|
+
此处不写第二套条件解析);`include_retired=False` 剔除 `state=='retired'` 的单元;
|
|
39
|
+
结果按 `(subject, slot)` 字典序稳定排序。
|
|
40
|
+
|
|
41
|
+
CLI(纯只读人工入口,**恒可用、无开关**):
|
|
42
|
+
python -X utf8 -m md_cg.state_slots --status
|
|
43
|
+
python -X utf8 -m md_cg.state_slots --subject 鲸娘 [--slot 住所]
|
|
44
|
+
数据根取 `MDCG_ROOT`(缺省走 `datapath.mdcg_root()`,与 `md_cg/sleep.py` 的
|
|
45
|
+
`_main` 同形)。本面与 stg 的 `MDCG_STG_STATE` 开关**无关**:开关管的是 MCP 读面,
|
|
46
|
+
CLI 是人工排查入口(只读、不改任何状态)。
|
|
47
|
+
|
|
48
|
+
不适用条件:本模块**不判语义**——「不是事实」的言语(玩笑/元层/回忆转述)之消歧
|
|
49
|
+
标记(`not_fact` 等)不在本批(P1 事件行无该信号);`alternatives`/`blindspots` 的
|
|
50
|
+
真值判定亦未落地(见上,恒空列表)。跨库/多租户合并不在此(单库=单台账)。
|
|
51
|
+
"""
|
|
52
|
+
from __future__ import annotations
|
|
53
|
+
|
|
54
|
+
import argparse
|
|
55
|
+
import json
|
|
56
|
+
import os
|
|
57
|
+
import sys
|
|
58
|
+
|
|
59
|
+
from . import state_events
|
|
60
|
+
|
|
61
|
+
#: 缺档位占位(台账未带 `tier` 时如实标注,不编造档位)。
|
|
62
|
+
TIER_UNSPECIFIED = "unspecified"
|
|
63
|
+
|
|
64
|
+
#: `history` 条目**必在**字段(单元 schema 逐字;缺键取 None,不改写数据)。
|
|
65
|
+
_HIST_FIELDS = ("t", "seq", "old", "new", "kind", "evidence", "actor")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# 生效条件:rec 为台账记录 dict——`rec.get("new")` 为 None 时返回 True(撤回/值作废),非 None 时返回 False;入参非 dict 时按 `.get` 抛 AttributeError(台账读面保证为 dict,不吞错)。
|
|
69
|
+
def _is_void(rec) -> bool:
|
|
70
|
+
"""撤回判据(单点):`new` 为空=值作废、无新值(口径见模块头)。"""
|
|
71
|
+
return rec.get("new") is None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# 生效条件:rec 为一条台账记录 dict 时,返回含 _HIST_FIELDS 七键(缺键取 None)的新 dict——字段集=单元 schema 的 history 条目逐字(不并入行内其余顶层键:新增键即「发明字段」,故限定枚举);值原样不改写、不改写入参。
|
|
75
|
+
def _hist_item(rec) -> dict:
|
|
76
|
+
"""变迁史单条:单元 schema 的七字段原样(值不改写)。"""
|
|
77
|
+
return {k: rec.get(k) for k in _HIST_FIELDS}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# 生效条件:subject/slot 为分组键、recs 为同组记录(**非空**,落盘序)、history 为真值时——按模块头回放规则返回单元 dict(含 history 键);history 为假值时同一单元但不带 history 键;recs 为空时抛 IndexError(组由记录现场建成,恒非空,不吞错)。
|
|
81
|
+
def _replay(subject, slot, recs, *, history=True) -> dict:
|
|
82
|
+
"""单组回放:现值 / 档位 / 区间 / 变迁史(规则逐条见模块头)。"""
|
|
83
|
+
value = None
|
|
84
|
+
last_valued = None # 最后**产生当前值**的事件下标(无则 None)
|
|
85
|
+
for i, rec in enumerate(recs):
|
|
86
|
+
if _is_void(rec):
|
|
87
|
+
value = None # 撤回:值作废、无新值
|
|
88
|
+
else:
|
|
89
|
+
value = rec.get("new")
|
|
90
|
+
last_valued = i
|
|
91
|
+
last = recs[-1]
|
|
92
|
+
retired = _is_void(last) # state:最后一个事件的形态决定
|
|
93
|
+
if last_valued is None:
|
|
94
|
+
# 全程无有值事件(孤立撤回):起端无可溯源 ⇒ None(不编造)。
|
|
95
|
+
t_from, seq_from = None, None
|
|
96
|
+
tier = TIER_UNSPECIFIED
|
|
97
|
+
else:
|
|
98
|
+
lv = recs[last_valued]
|
|
99
|
+
t_from, seq_from = lv.get("t"), lv.get("seq")
|
|
100
|
+
tier = lv.get("tier")
|
|
101
|
+
if tier is None:
|
|
102
|
+
tier = TIER_UNSPECIFIED
|
|
103
|
+
# 覆盖者:retired 态=撤回事件(最后一条,闭合点=值作废时刻);否则=有值事件的
|
|
104
|
+
# **下一条**事件(存在则闭合,不存在=None=未闭合);两者共用同一取舍。
|
|
105
|
+
nxt = (len(recs) - 1) if retired else (
|
|
106
|
+
None if last_valued is None else last_valued + 1)
|
|
107
|
+
if nxt is not None and 0 <= nxt < len(recs):
|
|
108
|
+
t_to, seq_to = recs[nxt].get("t"), recs[nxt].get("seq")
|
|
109
|
+
else:
|
|
110
|
+
t_to, seq_to = None, None
|
|
111
|
+
unit = {"subject": subject, "slot": slot, "value": value,
|
|
112
|
+
"state": "retired" if retired else "active",
|
|
113
|
+
"t_from": t_from, "t_to": t_to,
|
|
114
|
+
"seq_from": seq_from, "seq_to": seq_to,
|
|
115
|
+
"tier": tier,
|
|
116
|
+
# 结构在场、恒空:判定口径后置(模块头,不得臆造填充)。
|
|
117
|
+
"alternatives": [], "blindspots": [],
|
|
118
|
+
"event_count": len(recs)}
|
|
119
|
+
if history:
|
|
120
|
+
unit["history"] = [_hist_item(r) for r in recs]
|
|
121
|
+
return unit
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# 生效条件:recs 为台账记录序列(落盘序,可空)时——按 (subject, slot) 分组(组序=首现序)后按 (str(subject), str(slot)) 字典序稳定排序,逐组经 _replay;include_retired 为假时剔除 state=='retired' 的单元,为真时全留;返回单元列表(recs 为空返回 []);history 透传 _replay(假时不带 history 键)。
|
|
125
|
+
def _project(recs, *, include_retired=False, history=True) -> list:
|
|
126
|
+
"""记录序列 → 单元列表(project 与 status 共用的回放单点)。"""
|
|
127
|
+
groups = {}
|
|
128
|
+
for rec in recs:
|
|
129
|
+
key = (rec.get("subject"), rec.get("slot"))
|
|
130
|
+
groups.setdefault(key, []).append(rec)
|
|
131
|
+
# 排序键经 str() 归一:台账是外部可写面(append-only),非字符串 subject/slot 会让
|
|
132
|
+
# 裸元组比较抛 TypeError;字符串形态(正常)下与裸元组字典序逐位一致。
|
|
133
|
+
out = []
|
|
134
|
+
for key in sorted(groups, key=lambda k: (str(k[0]), str(k[1]))):
|
|
135
|
+
unit = _replay(key[0], key[1], groups[key], history=history)
|
|
136
|
+
if unit["state"] == "retired" and not include_retired:
|
|
137
|
+
continue
|
|
138
|
+
out.append(unit)
|
|
139
|
+
return out
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# 生效条件:cg 可解析出 root(经 state_events.ledger_path)时——以 state_events.read(cg, subject=subject, slot=slot) 为源(subject/slot 非 None 才过滤,空串亦为合法过滤值)、现算回放,返回单元列表;include_retired=False 剔除 state=='retired' 的单元;history=False 时单元不带 history 键;台账缺失/为空返回 [](不报错)。**零落盘、零缓存**——同一调用序列中台账追加一条,下一次调用结果即变。
|
|
143
|
+
def project(cg, subject=None, slot=None, include_retired=False,
|
|
144
|
+
history=True) -> list:
|
|
145
|
+
"""槽位寄存器投影(查询时现算):list[dict](单元字段见模块头)。
|
|
146
|
+
|
|
147
|
+
纯读函数:不改台账、不写盘、不缓存;过滤委托 `state_events.read`(单点),
|
|
148
|
+
本模块不另写一套条件解析。
|
|
149
|
+
"""
|
|
150
|
+
recs = state_events.read(cg, subject=subject, slot=slot)
|
|
151
|
+
return _project(recs, include_retired=include_retired, history=history)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# 生效条件:cg 可解析出 root 时——一次 state_events.read(cg) 后返回只读统计 dict:events(台账记录总数)/ subjects(去重 subject 数)/ slots(去重 slot 数)/ retired(回放后 state=='retired' 的单元数)/ bad_rows(state_events.BAD_ROWS 累计值,坏行采样与告警由该模块负责);台账缺失/为空时 events/subjects/slots/retired 皆 0。
|
|
155
|
+
def status(cg) -> dict:
|
|
156
|
+
"""台账与投影的只读读数(不计语义、不改任何状态)。
|
|
157
|
+
|
|
158
|
+
只读一次台账后回放:`bad_rows` 的口径是「累计**跳过动作次数**」(同一坏行每次
|
|
159
|
+
read 均计一次,见 `state_events`)——本函数每次调用只产生**一次** read。
|
|
160
|
+
`slots` 为去重**槽位名**数(同一槽位名挂在多个主体下只算一次);
|
|
161
|
+
`retired` 为回放后退役单元数(值已作废、未复活)。
|
|
162
|
+
"""
|
|
163
|
+
recs = state_events.read(cg)
|
|
164
|
+
units = _project(recs, include_retired=True, history=False)
|
|
165
|
+
return {"events": len(recs),
|
|
166
|
+
"subjects": len({r.get("subject") for r in recs}),
|
|
167
|
+
"slots": len({r.get("slot") for r in recs}),
|
|
168
|
+
"retired": sum(1 for u in units if u["state"] == "retired"),
|
|
169
|
+
"bad_rows": state_events.BAD_ROWS}
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# 生效条件:root 给定且 md_cg.mdcos 可导入时返回该 root 上的 MdCGOS 实例(形态同 md_cg/sleep.py:_open_cg);导入失败抛原异常。
|
|
173
|
+
def _open_cg(root: str):
|
|
174
|
+
from .mdcos import MdCGOS
|
|
175
|
+
return MdCGOS(root)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# 生效条件:传入 argv(None 取 sys.argv)——--status 时打印 status(cg) 的 JSON 并返回 0;否则要求 --subject(可配 --slot)或 --slot 单用,打印 project(cg, subject, slot, include_retired, history) 的 JSON 并返回 0;两者皆缺时打印**全量投影**(含退役需显式 --include-retired)并返回 0;--root 缺省取 datapath.mdcg_root()(env MDCG_ROOT);argparse 对未知参数照常以 SystemExit(2) 退出(不吞)。
|
|
179
|
+
def _main(argv=None) -> int:
|
|
180
|
+
"""只读人工入口:台账现状(--status)/ 槽位投影(--subject [--slot])。
|
|
181
|
+
|
|
182
|
+
本入口**恒可用**(不受 `MDCG_STG_STATE` 开关约束——那把开关管的是 MCP 读面);
|
|
183
|
+
全程只读:不 append、不落盘、不改任何状态。
|
|
184
|
+
"""
|
|
185
|
+
ap = argparse.ArgumentParser(
|
|
186
|
+
description="状态槽位投影(只读):台账现算,不落盘、不缓存")
|
|
187
|
+
ap.add_argument("--root", default=None, help="数据根(缺省 mdcg_root())")
|
|
188
|
+
ap.add_argument("--status", action="store_true",
|
|
189
|
+
help="只打印台账/投影统计(events/subjects/slots/retired/bad_rows)")
|
|
190
|
+
ap.add_argument("--subject", default=None, help="按主体过滤(省略=不过滤)")
|
|
191
|
+
ap.add_argument("--slot", default=None, help="按槽位过滤(省略=不过滤)")
|
|
192
|
+
ap.add_argument("--include-retired", dest="include_retired",
|
|
193
|
+
action="store_true", help="含退役单元(缺省剔除)")
|
|
194
|
+
ap.add_argument("--no-history", dest="history", action="store_false",
|
|
195
|
+
help="不带变迁史(history 键省略)")
|
|
196
|
+
a = ap.parse_args(argv)
|
|
197
|
+
try:
|
|
198
|
+
# 进程内调用(守卫/嵌入)会重定向 stdout:无 reconfigure 的流上跳过,
|
|
199
|
+
# 不改变任何输出内容。
|
|
200
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
201
|
+
except Exception: # noqa: BLE001
|
|
202
|
+
pass
|
|
203
|
+
root = os.path.abspath(a.root or _default_root())
|
|
204
|
+
cg = _open_cg(root)
|
|
205
|
+
if a.status:
|
|
206
|
+
out = status(cg)
|
|
207
|
+
else:
|
|
208
|
+
out = project(cg, subject=a.subject, slot=a.slot,
|
|
209
|
+
include_retired=a.include_retired, history=a.history)
|
|
210
|
+
print(json.dumps(out, ensure_ascii=False, indent=2))
|
|
211
|
+
return 0
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# 生效条件:无入参;返回 datapath.mdcg_root()(env MDCG_ROOT 非空时取该值,否则 paths.json/缺省),失败时抛原异常(不吞)。
|
|
215
|
+
def _default_root() -> str:
|
|
216
|
+
from .datapath import mdcg_root
|
|
217
|
+
return mdcg_root()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
if __name__ == "__main__":
|
|
221
|
+
sys.exit(_main())
|
package/md_cg/stg.py
CHANGED
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
anchors(...) 落在给定时间窗 / 空间范围内的节点
|
|
13
13
|
consistency() 时空字段自洽性检查
|
|
14
14
|
|
|
15
|
+
第 5 op(本批):`state_chain(cg, subject, slot, …)`——L2 **状态槽位投影**
|
|
16
|
+
(某主体某槽位的现值/区间/变迁史;flag `MDCG_STG_STATE`,**默认关**)。语义单点
|
|
17
|
+
在 `md_cg/state_slots.py`(投影不是第二真源:查询时从 append-only 台账现算)。
|
|
18
|
+
|
|
15
19
|
**分层(issue #52 线)**:
|
|
16
20
|
· 第 1/2 层(已收口):条件先于限额 + 截断可观测——`_scan` 只做「遍历 +
|
|
17
21
|
layer/可见性过滤」,条件过滤与 `_cap_hits` 截断归各接口。
|
|
@@ -28,6 +32,7 @@ from __future__ import annotations
|
|
|
28
32
|
|
|
29
33
|
import os
|
|
30
34
|
|
|
35
|
+
from . import lifecycle
|
|
31
36
|
from . import stgidx
|
|
32
37
|
from . import trust
|
|
33
38
|
|
|
@@ -130,7 +135,7 @@ def _node(cg, node_id):
|
|
|
130
135
|
"content": n.get("content") or ""}
|
|
131
136
|
|
|
132
137
|
|
|
133
|
-
# 生效条件:nid/e 为一条快照条目、layer 为层过滤值时——layer 为真值且 e.get("layer") != layer 即返回 None;cg 带可调用的 _readable(MdCGSecure)且判不可见即返回 None;e 含 "temporal" 或 "spatial" 键时以快照字段构造 frontmatter,否则调用 cg._read(e) 且在 fm 为 None 时返回 None;返回 {"id","frontmatter","layer","path"}。
|
|
138
|
+
# 生效条件:nid/e 为一条快照条目、layer 为层过滤值时——layer 为真值且 e.get("layer") != layer 即返回 None;lifecycle.is_archived(e) 为真即返回 None(退役不参与默认检索,判据单点在 lifecycle.py);cg 带可调用的 _readable(MdCGSecure)且判不可见即返回 None;e 含 "temporal" 或 "spatial" 键时以快照字段构造 frontmatter,否则调用 cg._read(e) 且在 fm 为 None 时返回 None;返回 {"id","frontmatter","layer","path"}。
|
|
134
139
|
def _scan_one(cg, nid, e, layer=None):
|
|
135
140
|
"""单条目 → 候选条目(`_scan` 的逐条实现**单点**:全量遍历与索引子集共用)。
|
|
136
141
|
|
|
@@ -140,6 +145,16 @@ def _scan_one(cg, nid, e, layer=None):
|
|
|
140
145
|
"""
|
|
141
146
|
if layer and e.get("layer") != layer:
|
|
142
147
|
return None
|
|
148
|
+
if lifecycle.is_archived(e):
|
|
149
|
+
return None # lifecycle.py:42 标称兑现:archived 不参与默认检索。
|
|
150
|
+
# 本处是 **stg 扫描面单点**(`_scan`(:172) 的全量遍历与
|
|
151
|
+
# 索引子集两条分支都经本函数逐条实现)——timeline /
|
|
152
|
+
# anchors / consistency 三 op 的候选面同点覆盖;第 5 op
|
|
153
|
+
# `state_chain` 走 append-only 台账、不经节点扫描,不受影响;
|
|
154
|
+
# `relation` 面是显式 id 直读(`_node`→`cg.get`),也不经此。
|
|
155
|
+
# 判据单点在 lifecycle.is_archived(fail-open:缺键=active
|
|
156
|
+
# 照常;只剔 archived——converged/demoted 是降权轴仍参与)。
|
|
157
|
+
# 直读/审计/恢复面不经本函数:退役不删除、可显式恢复。
|
|
143
158
|
_sec = getattr(cg, "_readable", None)
|
|
144
159
|
if _sec is not None and not _sec(e):
|
|
145
160
|
return None
|
|
@@ -164,7 +179,7 @@ def _scan_one(cg, nid, e, layer=None):
|
|
|
164
179
|
"path": e.get("path")}
|
|
165
180
|
|
|
166
181
|
|
|
167
|
-
# 生效条件:nodes 为 None 时**逐条**遍历 cg.index["nodes"] 全部条目(不按索引序切片、不读正文);nodes 为 (nid, entry) 对的序列时只遍历该序列(第 3 层索引子集,序由调用方保证=索引物理序);两种形态都逐条经 _scan_one(layer 过滤 + 可见性 + 条目化同一单点);返回 out 列表(全部 layer/可见性命中,**不做截断**——截断由各接口在条件过滤之后经 _cap_hits 执行,issue #52);
|
|
182
|
+
# 生效条件:nodes 为 None 时**逐条**遍历 cg.index["nodes"] 全部条目(不按索引序切片、不读正文);nodes 为 (nid, entry) 对的序列时只遍历该序列(第 3 层索引子集,序由调用方保证=索引物理序);两种形态都逐条经 _scan_one(layer 过滤 + 退役剔除 + 可见性 + 条目化同一单点);返回 out 列表(全部 layer/可见性命中,**不做截断**——截断由各接口在条件过滤之后经 _cap_hits 执行,issue #52);
|
|
168
183
|
def _scan(cg, layer=None, nodes=None):
|
|
169
184
|
"""遍历节点:时空字段直接读索引快照(不读文件,O(1)/节点)。
|
|
170
185
|
|
|
@@ -174,6 +189,10 @@ def _scan(cg, layer=None, nodes=None):
|
|
|
174
189
|
时逐条过读可见性——密级 × 会话绑定档在此与 _candidates 同口径,
|
|
175
190
|
stg 各 op(timeline/relation/anchors)不得成为绕过路径。可见性判定
|
|
176
191
|
**先于一切**(含截断):不可见节点既不进候选、也不占 kept 名额。
|
|
192
|
+
退役单点(本轮):`lifecycle.is_archived` 为真的节点同样不进候选
|
|
193
|
+
(判据单点在 lifecycle.py,与 cg 读面 `MdCGOS._candidates` 共引一份),
|
|
194
|
+
两个分支共用的逐条实现单点即 `_scan_one`——flag 开(索引子集)/关
|
|
195
|
+
(全量遍历)逐位一致,不因取数面不同而漏一次过滤。
|
|
177
196
|
|
|
178
197
|
issue #52(条件先行于限额):旧实现在此处 `list(index["nodes"].items())[:max_scan]`
|
|
179
198
|
——按 id 字典序在**条件过滤之前**砍尾巴,库超 max_scan 后(a)本会话记忆等条件
|
|
@@ -269,6 +288,8 @@ def _with_scan_reads(out, *, scanned, hits, kept, truncated, max_scan,
|
|
|
269
288
|
_INDEX_ENV = "MDCG_STG_INDEX"
|
|
270
289
|
#: 条件资格首验开关(只上报不过滤)。
|
|
271
290
|
_QUALIFY_ENV = "MDCG_STG_QUALIFY"
|
|
291
|
+
#: 状态槽位投影开关(第 5 op;与上两把同纪律:**默认关**,探针全绿后逐档开)。
|
|
292
|
+
_STATE_ENV = "MDCG_STG_STATE"
|
|
272
293
|
|
|
273
294
|
|
|
274
295
|
# 生效条件:环境变量 name 取值属 ("1","true","True") 时返回 True,其余(含未设/其它值)返回 False——默认关的开关一律走本判据(与 MDCG_LEGACY_ENV_AUTH 同形)。
|
|
@@ -699,4 +720,48 @@ def consistency(cg, layer=None, limit=50, max_scan=5000, time_axis="observed"):
|
|
|
699
720
|
return _with_scan_reads(
|
|
700
721
|
{"issues": len(issues), "limit": limit, "items": issues[:limit]},
|
|
701
722
|
scanned=scanned, hits=scanned, kept=kept, truncated=truncated,
|
|
702
|
-
max_scan=max_scan, index_meta=imeta)
|
|
723
|
+
max_scan=max_scan, index_meta=imeta)
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
# ---------------------------------------------------------------------------
|
|
727
|
+
# 第 5 op:状态槽位投影(flag MDCG_STG_STATE,默认关)
|
|
728
|
+
# ---------------------------------------------------------------------------
|
|
729
|
+
|
|
730
|
+
# 生效条件:_flag_on(_STATE_ENV) 为假(含未设)时恒返回 {"error":"disabled","hint":…}(不抛异常、不静默降级、不触台账);为真时以 state_slots.project(cg, subject, slot, include_retired, history) 为 items 全量(查询时现算,零落盘零缓存),limit 经 int() 归一(负数归 0)后取前 limit 条,返回含 count(命中总数,截断前)/subject/slot/include_retired/limit/items/truncated(count > kept)/kept(len(items))的 dict;project 抛异常时**不吞**(原样上抛)。
|
|
731
|
+
def state_chain(cg, subject=None, slot=None, include_retired=False, history=True,
|
|
732
|
+
limit=50):
|
|
733
|
+
"""状态槽位投影(第 5 op):某主体某槽位的**现值 / 区间 / 变迁史**。
|
|
734
|
+
|
|
735
|
+
语义单点在 `md_cg/state_slots.py`(**投影不是第二真源**:查询时从 append-only
|
|
736
|
+
台账 `<root>/_state_events.jsonl` 现算,不落盘、不缓存)——本函数只做开关与
|
|
737
|
+
返回体成形,不复制任何回放逻辑(改投影只改那一处)。
|
|
738
|
+
|
|
739
|
+
开关(`MDCG_STG_STATE`,**默认关**,与 `MDCG_STG_INDEX`/`MDCG_STG_QUALIFY`
|
|
740
|
+
同纪律):关臂返回 `{"error": "disabled", "hint": …}`——**不是异常**,是「本面
|
|
741
|
+
未启用」的如实答复;不静默降级成空结果(「没开」与「没命中」必须可分辨)。
|
|
742
|
+
|
|
743
|
+
返回体沿 stg 既有口径(issue #52:截断可观测):`count` 为**条件命中总数**
|
|
744
|
+
(截断前,不受 limit 影响)、`items` 为前 `limit` 条、`kept` 为实际返回条数、
|
|
745
|
+
`truncated` 为 `count > kept`(limit=0 ⇒ items 空、truncated 随 count 真值)。
|
|
746
|
+
|
|
747
|
+
不适用条件:`time_axis` 不适用于本 op(台账事件行只有落账时间戳 `t`,无
|
|
748
|
+
双轴端点——本面**不伪造**第二轴;需要轴语义请走 timeline/relation/anchors);
|
|
749
|
+
`alternatives`/`blindspots` 本批恒空列表(判定口径后置,见 `state_slots` 模块头)。
|
|
750
|
+
"""
|
|
751
|
+
if not _flag_on(_STATE_ENV):
|
|
752
|
+
return {"error": "disabled",
|
|
753
|
+
"hint": "本面默认关:设 MDCG_STG_STATE=1 显式启用"
|
|
754
|
+
"(与 MDCG_STG_INDEX/MDCG_STG_QUALIFY 同纪律)"}
|
|
755
|
+
from . import state_slots
|
|
756
|
+
units = state_slots.project(cg, subject=subject, slot=slot,
|
|
757
|
+
include_retired=include_retired,
|
|
758
|
+
history=history)
|
|
759
|
+
total = len(units)
|
|
760
|
+
cap = int(limit) if limit is not None else 0
|
|
761
|
+
if cap < 0:
|
|
762
|
+
cap = 0
|
|
763
|
+
items = units[:cap]
|
|
764
|
+
return {"count": total, "subject": subject, "slot": slot,
|
|
765
|
+
"include_retired": include_retired, "limit": limit,
|
|
766
|
+
"items": items, "truncated": total > len(items),
|
|
767
|
+
"kept": len(items)}
|
package/md_cg/sustain.py
CHANGED
|
@@ -71,6 +71,17 @@ DEFAULT_SCRUB_INTERVAL = 3600.0 # 记忆自净(抽查/去污染/校准)1h
|
|
|
71
71
|
DEFAULT_EVOLVE_INTERVAL = 7200.0 # 演化巡检(固化/重要性候选盘点)2h;只读
|
|
72
72
|
DEFAULT_TIDY_INTERVAL = 21600.0 # 整理巡检(contextual 同构组聚合)6h
|
|
73
73
|
|
|
74
|
+
#: 六档 tick 的(档名 → interval 属性名)映射——进度面与 stale 阈值的取值面,
|
|
75
|
+
#: 顺序即 `SustainLoop._run()` 的执行序(单一真源:改档名/加点只改这里)。
|
|
76
|
+
TICK_INTERVAL_ATTRS = (("beat", "beat_interval"), ("heal", "heal_interval"),
|
|
77
|
+
("scrub", "scrub_interval"), ("evolve", "evolve_interval"),
|
|
78
|
+
("tidy", "tidy_interval"), ("sleep", "sleep_interval"))
|
|
79
|
+
#: 活体进度面的 stale 阈值下限(秒)——issue #63:某档 tick 运行超过
|
|
80
|
+
#: `max(2×该档 interval, TICK_STALE_MIN_S)` 时,状态面显式给 `stale_tick`
|
|
81
|
+
#: 告警。**只上报,不杀线程**(线程不可安全强杀;处置=人工重启常驻进程,
|
|
82
|
+
#: 见 `docs/mdcg/睡眠周期_运维前提与维护指南_v1.0.md`)。
|
|
83
|
+
TICK_STALE_MIN_S = 1800.0
|
|
84
|
+
|
|
74
85
|
# ---- 四档 `auto_*` 缺省的**单一真源**(P0-2,2026-10-01)--------------------
|
|
75
86
|
# 为什么要有这张表:同一组缺省此前在**三处**各写一份——op 路径
|
|
76
87
|
# (mcp_server.py 的 `_sustain_call` start 分支)、env 路径(`_start_sustain`)
|
|
@@ -1077,6 +1088,13 @@ class SustainLoop:
|
|
|
1077
1088
|
self.tidys = []
|
|
1078
1089
|
# 数据健康不变量断言集结论(Pi⑤):与 tidy 同节奏,只取结论不落盘
|
|
1079
1090
|
self.last_conformance = None
|
|
1091
|
+
# 活体进度面(issue #63):当前档 / 上一档完成 / 上一档错误——
|
|
1092
|
+
# 写点 = 既有心跳戳(见 `_progress_fields` / `_flush_progress`),
|
|
1093
|
+
# **不造第二套状态文件**。
|
|
1094
|
+
self.current_tick = None
|
|
1095
|
+
self.last_tick_done = None
|
|
1096
|
+
self.last_tick_error = None
|
|
1097
|
+
self._ticks_wrapped = False # 六档进度面包装的幂等标记
|
|
1080
1098
|
self._started_at = None
|
|
1081
1099
|
self._th = None
|
|
1082
1100
|
self._stop = threading.Event()
|
|
@@ -1084,18 +1102,126 @@ class SustainLoop:
|
|
|
1084
1102
|
|
|
1085
1103
|
# ---- 心跳 ----
|
|
1086
1104
|
|
|
1087
|
-
# 生效条件:task_running 非 None 时先置 self.task_running=bool(task_running),再 write_stamp(self.name, self.d, task_running=self.task_running, root=self.cg.root, uptime=... if self._started_at else 0.0),beats 自增并记 last_beat=rec["ts"],返回该 rec;
|
|
1105
|
+
# 生效条件:task_running 非 None 时先置 self.task_running=bool(task_running),再 write_stamp(self.name, self.d, task_running=self.task_running, root=self.cg.root, uptime=... if self._started_at else 0.0, **进度三字段),beats 自增并记 last_beat=rec["ts"],返回该 rec;
|
|
1088
1106
|
def beat(self, task_running: bool = None) -> dict:
|
|
1089
1107
|
if task_running is not None:
|
|
1090
1108
|
self.task_running = bool(task_running)
|
|
1091
1109
|
rec = write_stamp(
|
|
1092
1110
|
self.name, self.d, task_running=self.task_running, root=self.cg.root,
|
|
1093
1111
|
uptime=round(time.time() - self._started_at, 3)
|
|
1094
|
-
if self._started_at else 0.0
|
|
1112
|
+
if self._started_at else 0.0,
|
|
1113
|
+
**self._progress_fields()) # 进度面随心跳同落(单点状态面)
|
|
1095
1114
|
self.beats += 1
|
|
1096
1115
|
self.last_beat = rec["ts"]
|
|
1097
1116
|
return rec
|
|
1098
1117
|
|
|
1118
|
+
# ---- 活体进度面(issue #63)----
|
|
1119
|
+
#
|
|
1120
|
+
# 为什么有这一段:`_run()` 单线程串行六档,任一档**无界阻塞**即全循环停摆
|
|
1121
|
+
# (实测停 9.5 小时:心跳/自愈/巡检/演化/整理/睡眠全停)。卡住期间外部
|
|
1122
|
+
# **无法知道卡在哪档**——tick 台账在 run_cycle **返回后**才写。故:
|
|
1123
|
+
# · 进入/退出每档各刷一次**既有状态面**(心跳戳)——不造第二套状态文件;
|
|
1124
|
+
# · `current_tick` 给出「此刻在哪档、已跑多久」,`stale_tick` 超限告警;
|
|
1125
|
+
# · 六档异常**不再静默**(容忍≠静默):`last_tick_error` 记类名 + 摘要。
|
|
1126
|
+
# 形态:全部经 `_wrap_ticks()` **单点包装**——`_run` 的六档 try/except 逐字
|
|
1127
|
+
# 未改(`md_cg/test_sleep_p1.py` G3a 把它钉死;包装让异常在进入 `_run` 的
|
|
1128
|
+
# except 之前就已被记录,那边的 `pass` 保留为兜底)。
|
|
1129
|
+
|
|
1130
|
+
# 生效条件:无入参;返回进度三字段 dict(current_tick / last_tick_done / last_tick_error;None 值由 write_stamp 丢弃)——写戳与 status 读取的唯一取值面。
|
|
1131
|
+
def _progress_fields(self) -> dict:
|
|
1132
|
+
"""进度三字段(写点与读取的**单一取值面**)。"""
|
|
1133
|
+
return {"current_tick": self.current_tick,
|
|
1134
|
+
"last_tick_done": self.last_tick_done,
|
|
1135
|
+
"last_tick_error": self.last_tick_error}
|
|
1136
|
+
|
|
1137
|
+
# 生效条件:以 write_stamp(self.name, self.d, task_running=self.task_running, root=self.cg.root, uptime=... if self._started_at else 0.0, **进度三字段) 刷新既有心跳戳(原子替换);写失败静默(与心跳同纪律)——进度面缺失是已知边界(写失败时 status 的当前档读数退化为 null)。
|
|
1138
|
+
def _flush_progress(self):
|
|
1139
|
+
"""把进度面刷新进**既有状态面**(心跳戳)——不新建状态文件。
|
|
1140
|
+
|
|
1141
|
+
为什么**进入**档时就要刷:卡死场景下「没有下一次写」正是常态——进度
|
|
1142
|
+
必须在进入时就落盘,否则观测面恰好缺了要观测的那一刻。写失败静默
|
|
1143
|
+
(与 `beat()` 同纪律),但失败即拉不到进度,属已知边界。
|
|
1144
|
+
"""
|
|
1145
|
+
try:
|
|
1146
|
+
write_stamp(self.name, self.d, task_running=self.task_running,
|
|
1147
|
+
root=self.cg.root,
|
|
1148
|
+
uptime=round(time.time() - self._started_at, 3)
|
|
1149
|
+
if self._started_at else 0.0,
|
|
1150
|
+
**self._progress_fields())
|
|
1151
|
+
except Exception: # noqa: BLE001
|
|
1152
|
+
pass # 进度面刷新失败不拖垮常驻
|
|
1153
|
+
|
|
1154
|
+
# 生效条件:无入参;把六档 tick 方法(beat / _tick_heal / _tick_scrub / _tick_evolve / _tick_tidy / _tick_sleep)就地包上 _wrap_tick(幂等:_ticks_wrapped 为真即直接返回 self);返回 self。
|
|
1155
|
+
def _wrap_ticks(self):
|
|
1156
|
+
"""把六档 tick 包上进度面(幂等)——**不改 `_run` 的逐字形态**。
|
|
1157
|
+
|
|
1158
|
+
为什么用包装而不是改 `_run` 的六档结构:`_run` 的六档 try/except 形态
|
|
1159
|
+
被既有守卫**逐字**钉死(`md_cg/test_sleep_p1.py` G3a「第六档形态与
|
|
1160
|
+
既有五档逐字同构(try/except 吞异常 + 末尾 _stop.wait(_POLL))」),
|
|
1161
|
+
而 issue #63 要求的「进/出留读数 + 异常不静默」是**每档同款**动作——
|
|
1162
|
+
单点包装既保住既有形态(六档语义与顺序一字不动),又免六处重复
|
|
1163
|
+
(改档名/加点只动 `TICK_INTERVAL_ATTRS`)。
|
|
1164
|
+
"""
|
|
1165
|
+
if getattr(self, "_ticks_wrapped", False):
|
|
1166
|
+
return self
|
|
1167
|
+
for name, _attr in TICK_INTERVAL_ATTRS:
|
|
1168
|
+
meth = "_tick_" + name if name != "beat" else "beat"
|
|
1169
|
+
setattr(self, meth, self._wrap_tick(name, getattr(self, meth)))
|
|
1170
|
+
self._ticks_wrapped = True
|
|
1171
|
+
return self
|
|
1172
|
+
|
|
1173
|
+
# 生效条件:name 为该档名、fn 为该档可调用;返回包装函数 wrapped——调用时先写 current_tick={name, started_at, pid} 并刷新戳,调 fn(*a, **kw),异常记入 last_tick_error={name, error: 类名+摘要(截 200 字符), t} 并**不重抛**(容忍≠静默;`_run` 的既有 except 保留为兜底),随后清 current_tick、记 last_tick_done={name, t, ok} 并再刷新戳。
|
|
1174
|
+
def _wrap_tick(self, name: str, fn):
|
|
1175
|
+
"""单档 tick 的**进度面包裹**(issue #63):进/出各留读数,异常不静默。
|
|
1176
|
+
|
|
1177
|
+
「容忍 ≠ 静默」:六档的 except 面此前只有 `pass`——失败的档在外部
|
|
1178
|
+
读不到。现在失败一律进 `last_tick_error`(类名 + 摘要);卡住靠
|
|
1179
|
+
`current_tick` 的年龄与 `stale_tick` 告警判(不杀线程)。
|
|
1180
|
+
"""
|
|
1181
|
+
def wrapped(*a, **kw):
|
|
1182
|
+
self.current_tick = {"name": name, "started_at": time.time(),
|
|
1183
|
+
"pid": os.getpid()}
|
|
1184
|
+
self._flush_progress()
|
|
1185
|
+
ok_ = True
|
|
1186
|
+
try:
|
|
1187
|
+
fn(*a, **kw)
|
|
1188
|
+
except Exception as e: # noqa: BLE001
|
|
1189
|
+
ok_ = False
|
|
1190
|
+
self.last_tick_error = {"name": name, "t": time.time(),
|
|
1191
|
+
"error": "%s: %s" % (type(e).__name__,
|
|
1192
|
+
str(e)[:200])}
|
|
1193
|
+
self.current_tick = None
|
|
1194
|
+
self.last_tick_done = {"name": name, "t": time.time(), "ok": ok_}
|
|
1195
|
+
self._flush_progress()
|
|
1196
|
+
return ok_
|
|
1197
|
+
wrapped.__name__ = "wrapped_%s_tick" % name
|
|
1198
|
+
return wrapped
|
|
1199
|
+
|
|
1200
|
+
# 生效条件:ct 为 current_tick 形态(含 name 与 started_at)且 name 在 TICK_INTERVAL_ATTRS 内时,以该档 interval 算 lim=max(2×interval, TICK_STALE_MIN_S),年龄(now-started_at)超 lim 返回 {tick, age_s, limit_s, alert}(alert 文案含档名与已运行时长);未超或形态不符返回 None。
|
|
1201
|
+
def _stale_tick(self, ct):
|
|
1202
|
+
"""`current_tick` 的 stale 判定:年龄 > `max(2×该档 interval, 1800s)`。
|
|
1203
|
+
|
|
1204
|
+
超限**只告警不杀线程**(线程不可安全强杀;处置=人工重启常驻进程,
|
|
1205
|
+
见运维指南「常驻循环的卡死防护与观测」一节)。
|
|
1206
|
+
"""
|
|
1207
|
+
if not isinstance(ct, dict):
|
|
1208
|
+
return None
|
|
1209
|
+
name = ct.get("name")
|
|
1210
|
+
attr = dict(TICK_INTERVAL_ATTRS).get(name)
|
|
1211
|
+
if attr is None:
|
|
1212
|
+
return None
|
|
1213
|
+
try:
|
|
1214
|
+
lim = max(2.0 * float(getattr(self, attr)), TICK_STALE_MIN_S)
|
|
1215
|
+
age = max(0.0, time.time() - float(ct.get("started_at") or 0.0))
|
|
1216
|
+
except (TypeError, ValueError):
|
|
1217
|
+
return None
|
|
1218
|
+
if age <= lim:
|
|
1219
|
+
return None
|
|
1220
|
+
return {"tick": name, "age_s": round(age, 1), "limit_s": round(lim, 1),
|
|
1221
|
+
"alert": ("档 %s 已运行 %.0fs(阈值 %.0fs = max(2×间隔, %.0fs))"
|
|
1222
|
+
"——疑似卡死;不自动杀线程,处置=人工重启常驻进程"
|
|
1223
|
+
% (name, age, lim, TICK_STALE_MIN_S))}
|
|
1224
|
+
|
|
1099
1225
|
# ---- 生命周期 ----
|
|
1100
1226
|
|
|
1101
1227
|
# 生效条件:不适用(无必需形参与模块级常量)
|
|
@@ -1119,8 +1245,9 @@ class SustainLoop:
|
|
|
1119
1245
|
clear_stamp(self.name, self.d)
|
|
1120
1246
|
return self
|
|
1121
1247
|
|
|
1122
|
-
# 生效条件:self._stop
|
|
1248
|
+
# 生效条件:self._stop 未置位期间轮询(**先调 _wrap_ticks 给六档包上进度面,幂等**),按 beat_interval/heal_interval/scrub_interval/evolve_interval/tidy_interval/**sleep_interval(第六档)** 到期分别执行 beat 与 _tick_heal/_tick_scrub/_tick_evolve/_tick_tidy/_tick_sleep(六档调用形态与既有五档逐字同构:try/except 吞异常 + `_stop.wait(_POLL)` 收尾;进/出读数与异常记录由包装单点提供——issue #63);
|
|
1123
1249
|
def _run(self):
|
|
1250
|
+
self._wrap_ticks() # issue #63:六档包上进度面(幂等,形态不变)
|
|
1124
1251
|
next_beat = time.time() + self.beat_interval
|
|
1125
1252
|
next_heal = time.time() + self.heal_interval
|
|
1126
1253
|
next_scrub = time.time() + self.scrub_interval
|
|
@@ -1309,12 +1436,25 @@ class SustainLoop:
|
|
|
1309
1436
|
|
|
1310
1437
|
# ---- 状态 ----
|
|
1311
1438
|
|
|
1312
|
-
# 生效条件:以 read_stamp(self.name, self.d) 判定 state(有戳走 judge(age, interval=self.beat_interval, task_running=...),无戳为 "stopped"
|
|
1439
|
+
# 生效条件:以 read_stamp(self.name, self.d) 判定 state(有戳走 judge(age, interval=self.beat_interval, task_running=...),无戳为 "stopped");进度面(issue #63)同进程内存优先、跨进程回落戳内字段,current_tick 附 running_s(已运行秒数);stale_tick 在 current_tick 年龄超 max(2×该档 interval, 1800s) 时给出告警(含档名与时长);返回含 name/running/pid/uptime(无 _started_at 时为 0.0)/beats/last_beat/各 interval 与 auto_* 开关/heals[-5:]/evolves[-5:]/tidys[-5:]/current_tick/last_tick_done/last_tick_error/stale_tick/peers(self.d)/ledger.summary() 的 dict;
|
|
1313
1440
|
def status(self) -> dict:
|
|
1314
1441
|
st = read_stamp(self.name, self.d)
|
|
1315
1442
|
state = (judge(st["age"], interval=self.beat_interval,
|
|
1316
1443
|
task_running=bool(st.get("task_running")))
|
|
1317
1444
|
if st else "stopped")
|
|
1445
|
+
# 进度面(issue #63):同进程内存优先(最实时),跨进程回落戳内字段
|
|
1446
|
+
# ——戳是**既有状态面**(不造第二套状态文件);外部 `action=beat`
|
|
1447
|
+
# 覆盖戳时内存侧不受影响。
|
|
1448
|
+
cur = self.current_tick or (st or {}).get("current_tick")
|
|
1449
|
+
if isinstance(cur, dict):
|
|
1450
|
+
cur = dict(cur)
|
|
1451
|
+
try:
|
|
1452
|
+
cur["running_s"] = round(max(
|
|
1453
|
+
0.0, time.time() - float(cur.get("started_at") or 0.0)), 1)
|
|
1454
|
+
except (TypeError, ValueError):
|
|
1455
|
+
cur["running_s"] = None
|
|
1456
|
+
last_done = self.last_tick_done or (st or {}).get("last_tick_done")
|
|
1457
|
+
last_err = self.last_tick_error or (st or {}).get("last_tick_error")
|
|
1318
1458
|
return {"name": self.name, "running": bool(self._th
|
|
1319
1459
|
and self._th.is_alive()),
|
|
1320
1460
|
"pid": os.getpid(),
|
|
@@ -1344,6 +1484,12 @@ class SustainLoop:
|
|
|
1344
1484
|
"last_sleep": self.last_sleep,
|
|
1345
1485
|
"sleeps": self.sleeps[-5:],
|
|
1346
1486
|
"last_conformance": self.last_conformance,
|
|
1487
|
+
# 活体进度面(issue #63):此刻在哪档 / 上一档完成 / 上一档错误
|
|
1488
|
+
# / 超限告警(只上报,不杀线程)。
|
|
1489
|
+
"current_tick": cur,
|
|
1490
|
+
"last_tick_done": last_done,
|
|
1491
|
+
"last_tick_error": last_err,
|
|
1492
|
+
"stale_tick": self._stale_tick(cur),
|
|
1347
1493
|
"peers": peers(self.d),
|
|
1348
1494
|
"sessions": self.ledger.summary()}
|
|
1349
1495
|
|