@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.
Files changed (30) hide show
  1. package/README.md +631 -630
  2. package/codebuddy/CODEBUDDY.md +5 -5
  3. package/docs/discipline/templates/zcode-user.md.tmpl +4 -1
  4. 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
  5. 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
  6. 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
  7. 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
  8. 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
  9. 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
  10. 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
  11. package/lib/hooks.d.ts +34 -0
  12. package/lib/hooks.js +165 -0
  13. package/lib/index.js +22 -1
  14. package/lib/lib/mdcg_client.d.ts +37 -0
  15. package/lib/lib/mdcg_client.js +44 -0
  16. package/md_cg/audit.py +7 -1
  17. package/md_cg/bench_governance.py +4 -1
  18. package/md_cg/bench_locomo_zh.py +12 -2
  19. package/md_cg/bench_zh_mad.py +12 -2
  20. package/md_cg/interop.py +25 -9
  21. package/md_cg/sleep.py +223 -103
  22. package/md_cg/sustain.py +150 -4
  23. package/md_cg/test_sustain_bounded.py +624 -0
  24. package/md_cg/units.py +4 -1
  25. package/package.json +1 -1
  26. package/skills/plugin.json +1 -1
  27. package/src/hooks.ts +177 -0
  28. package/src/index.ts +22 -1
  29. package/src/lib/mdcg_client.ts +48 -0
  30. 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 未置位期间轮询,按 beat_interval/heal_interval/scrub_interval/evolve_interval/tidy_interval/**sleep_interval(第六档)** 到期分别执行 beat 与 _tick_heal/_tick_scrub/_tick_evolve/_tick_tidy/_tick_sleep,各 tick 抛出的异常被吞掉不中断循环,末尾以 _stop.wait(_POLL) 休眠;
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"),返回含 name/running/pid/uptime(无 _started_at 时为 0.0)/beats/last_beat/各 interval 与 auto_* 开关/heals[-5:]/evolves[-5:]/tidys[-5:]/peers(self.d)/ledger.summary() 的 dict;
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