@furongjun1999/dsh-memory 0.7.4 → 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 -630
- 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 +1 -1
- 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/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 +165 -0
- package/lib/index.js +22 -1
- package/lib/lib/mdcg_client.d.ts +37 -0
- package/lib/lib/mdcg_client.js +44 -0
- 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/sleep.py +223 -103
- package/md_cg/sustain.py +150 -4
- package/md_cg/test_sustain_bounded.py +624 -0
- package/md_cg/units.py +4 -1
- package/package.json +1 -1
- package/skills/plugin.json +1 -1
- package/src/hooks.ts +177 -0
- package/src/index.ts +22 -1
- package/src/lib/mdcg_client.ts +48 -0
- package/zcode/AGENTS.md +5 -5
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
|
|