@furongjun1999/dsh-memory 0.6.1 → 0.7.1
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 +49 -18
- package/docs/README.md +1 -0
- package/docs/eval/cons200_/345/206/262/347/252/201/346/243/200/346/265/213/351/200/211/351/235/242_/345/256/236/346/226/275/350/256/260/345/275/225_v1.0.md +223 -0
- package/docs/eval/cons200_/345/206/262/347/252/201/346/243/200/346/265/213/351/200/211/351/235/242_/345/256/236/346/226/275/350/256/260/345/275/225_v1.1.md +340 -0
- package/docs/eval/issue50_/345/205/203/346/225/260/346/215/256/351/200/217/344/274/240/344/270/216/345/205/234/345/272/225_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +437 -0
- package/docs/eval/issue50_/345/215/212/351/207/215/345/244/215/345/276/205/345/256/232/345/244/215/346/240/270_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +488 -0
- package/docs/eval/issue50_/345/276/205/345/256/232/345/244/215/346/240/270/345/205/245/351/230/237_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +428 -0
- package/docs/eval/issue50_/350/257/273/351/235/242/344/277/235/346/212/244/345/217/252/350/256/244/346/230/276/345/274/217/346/235/245/346/272/220_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +271 -0
- package/docs/eval/issue50_/351/207/215/350/246/201/345/272/246/345/220/214/346/272/220/344/270/216/344/277/235/346/212/244/350/257/255/344/271/211_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +306 -0
- package/docs/eval/issue51_/344/270/200/351/224/256/345/256/211/350/243/205/345/244/261/350/264/245_/345/275/222/345/261/236/345/210/244/345/256/232_v1.0.md +58 -0
- package/docs/eval/issue52_/346/235/241/344/273/266/345/205/210/350/241/214/344/270/216/346/210/252/346/226/255/345/217/257/350/247/202/346/265/213_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +360 -0
- package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/241/346/241/243/344/275/215/345/215/225/344/270/200/345/205/245/345/217/243_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +651 -0
- package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/242/345/217/230/346/233/264/345/215/225/344/270/216/345/233/236/346/273/232/345/216/237/350/257/255_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +439 -0
- package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/243/345/207/206/345/205/245/350/257/273/346/225/260_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +1337 -0
- package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/244/346/224/266/345/256/230/344/270/216/345/205/250/351/223/276/351/252/214/346/224/266_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +1817 -0
- package/docs/eval//345/207/272/350/264/247/351/235/242/345/206/222/347/203/237_/350/277/233/350/264/247/351/227/250/347/246/201_v1.0.md +460 -0
- package/docs/eval//345/217/221/345/270/20308_/350/207/252/350/277/255/344/273/243/344/270/216/347/235/241/347/234/240_/345/233/276/346/243/200/347/264/242/350/267/257/344/270/216/346/235/203/351/207/215_v1.0.md +97 -0
- package/docs/eval//345/217/221/345/270/20309_/346/243/200/347/264/242/351/235/242/344/270/211/346/211/271/346/224/266/345/217/243_v1.0.md +66 -0
- package/docs/eval//345/275/222/344/270/200/345/261/202/347/274/272/347/234/201/347/277/273/345/205/263_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +458 -0
- package/docs/eval//347/254/2543/345/261/202stg/347/273/223/346/236/204/347/264/242/345/274/225_/345/256/236/346/226/275/350/256/260/345/275/225_v1.0.md +250 -0
- package/docs/hive//346/243/200/347/264/242/347/256/227/346/263/225/345/217/243/345/276/204/345/257/271/347/205/247_v0.1.md +136 -11
- package/docs/hive//346/243/200/347/264/242/350/267/257/345/276/204/344/270/216/350/256/244/347/237/245/347/273/223/346/236/204/345/245/221/347/272/246_v0.1.md +32 -2
- package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +88 -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 +40 -40
- package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +47 -11
- 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 +183 -0
- package/docs/plans/stg/346/235/241/344/273/266/345/214/226/344/270/216/347/273/223/346/236/204/347/264/242/345/274/225_/350/256/276/350/256/241_v0.1.md +124 -0
- package/docs/plans//347/235/241/347/234/240/344/270/216/350/207/252/350/277/255/344/273/243_/345/212/237/350/203/275/344/274/230/345/214/226/350/256/276/350/256/241_v0.4.md +547 -0
- package/docs/plans//350/256/260/345/277/206/350/207/252/345/244/204/347/220/206/344/270/211/346/241/243/350/207/252/346/262/273_/350/256/276/350/256/241_v0.2.md +207 -0
- package/md_cg/admission.py +718 -0
- package/md_cg/autonomy_modes.py +642 -0
- package/md_cg/bench_e2e_locomo_qa.py +11 -4
- package/md_cg/chain.py +47 -0
- package/md_cg/consistency.py +133 -8
- package/md_cg/forgetting.py +38 -6
- package/md_cg/freshness.py +527 -0
- package/md_cg/generation.py +409 -0
- package/md_cg/hotcache.py +4 -1
- package/md_cg/lifecycle.py +30 -2
- package/md_cg/mcp_server.py +191 -26
- package/md_cg/mdcg.py +594 -39
- package/md_cg/mdcos.py +869 -38
- package/md_cg/nodefile.py +74 -1
- package/md_cg/protect.py +79 -4
- package/md_cg/provenance.py +1 -1
- package/md_cg/review_cli.py +31 -2
- package/md_cg/rollback.py +411 -0
- package/md_cg/rollback_cli.py +104 -0
- package/md_cg/semantic/canonical.py +22 -0
- package/md_cg/semantic/unify.py +73 -22
- package/md_cg/semantic/unify_fixture.json +25 -0
- package/md_cg/sleep.py +1297 -0
- package/md_cg/stg.py +400 -50
- package/md_cg/stgidx.py +281 -0
- package/md_cg/sustain.py +146 -9
- package/md_cg/test_auto_defaults.py +424 -0
- package/md_cg/test_autonomy_admission.py +1098 -0
- package/md_cg/test_autonomy_modes.py +1972 -0
- package/md_cg/test_b1_auto_id_multiproc.py +7 -0
- package/md_cg/test_b1b2_write_face.py +7 -0
- package/md_cg/test_b3_merge_keeps_content.py +7 -0
- package/md_cg/test_boundary_hit.py +410 -0
- package/md_cg/test_cons200_scan_selection.py +791 -0
- package/md_cg/test_en_pipeline.py +22 -13
- package/md_cg/test_generation_guard.py +352 -0
- package/md_cg/test_h4_sustain_snapshot.py +14 -4
- package/md_cg/test_i50a_half_dup_defer.py +408 -0
- package/md_cg/test_i50b_defer_to_review_queue.py +545 -0
- package/md_cg/test_i50c_meta_passthrough.py +608 -0
- package/md_cg/test_i50d_importance_source.py +531 -0
- package/md_cg/test_i50e_readside_protection.py +656 -0
- package/md_cg/test_issue39_utf8_stdio.py +9 -1
- package/md_cg/test_issue52_scan_condition_first.py +677 -0
- package/md_cg/test_linkref.py +7 -0
- package/md_cg/test_mode_parity.py +1393 -0
- package/md_cg/test_mutation_rollback.py +805 -0
- package/md_cg/test_n204_n205_n226_n227_n228_n229_exit_gates.py +1 -1
- package/md_cg/test_n212_n213_n224_generation_gates.py +14 -8
- package/md_cg/test_n214_n215_n221_n222_write_face_gates.py +7 -0
- package/md_cg/test_n230_dirty_replay.py +375 -0
- package/md_cg/test_p2_mcp.py +8 -0
- package/md_cg/test_p2_six_elements.py +463 -0
- package/md_cg/test_p3_legacy_closure.py +433 -0
- package/md_cg/test_p4_freshness.py +680 -0
- package/md_cg/test_p8_subgraph_chain.py +14 -1
- package/md_cg/test_p9_forget_protect.py +7 -0
- package/md_cg/test_p9c_dedup_hints.py +7 -0
- package/md_cg/test_policy_required_ccg.py +5 -1
- package/md_cg/test_protocol.py +7 -0
- package/md_cg/test_rank_parity_score_mode.py +6 -0
- package/md_cg/test_semantic_canonical.py +5 -3
- package/md_cg/test_sleep.py +611 -0
- package/md_cg/test_sleep_p1.py +784 -0
- package/md_cg/test_stgidx_index_parity.py +1020 -0
- package/md_cg/test_time_core_lint.py +968 -0
- package/md_cg/test_unify_default_off.py +701 -0
- package/md_cg/test_unify_scope.py +182 -0
- package/md_cg/test_writelimit.py +36 -9
- package/md_cg/test_writepipe.py +5 -2
- package/md_cg/weights.py +15 -1
- package/md_cg/whitebox_kb/aeis_core/time_core.py +8 -0
- package/md_cg/writepipe.py +101 -2
- package/package.json +3 -2
- package/skills/plugin.json +1 -1
- package/utf8_boot.py +237 -0
package/md_cg/stgidx.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""stg 第 3 层:结构索引(装载期内存倒排 + 写路径增量维护)。
|
|
3
|
+
|
|
4
|
+
设计稿:`docs/plans/stg条件化与结构索引_设计_v0.1.md`(2026-10-03 使用者签收)。
|
|
5
|
+
本模块只放**结构与取数**:三张表的构建 / 增删 / 取子集 / 一致性自检。
|
|
6
|
+
|
|
7
|
+
「何时构建、何时回退、回退怎么上报」的编排在读侧(`stg.py`);
|
|
8
|
+
「何时调增删钩」的编排在写侧(`mdcg._set_index_entry` / `mdcg._remove_index_entry`)。
|
|
9
|
+
两处都只经本模块的单点,不另写第二份分类口径。
|
|
10
|
+
|
|
11
|
+
三张表(**不变量:恒为快照 keys 的划分**,见 `verify`):
|
|
12
|
+
|
|
13
|
+
· `by_session: {sid | None | _UNHASHABLE: [id, ...]}`——`None` = 无会话归属
|
|
14
|
+
(`unassigned` 兜底桶,语义与基线 ``fm.get("session") != sid`` 的等值比较一致);
|
|
15
|
+
· `by_layer: {layer | _UNHASHABLE: [id, ...]}`;
|
|
16
|
+
· `by_time: [(t, id), ...]` 升序——`t` 取**观察轴区间起点**,经既有单点
|
|
17
|
+
`trust.time_window_of`(= `stg._interval` 同一实现,不引入第二套时间解析);
|
|
18
|
+
无有效时间戳 → `untimed`;非有限值(NaN/±inf)→ `nonfinite`(见下)。
|
|
19
|
+
另加 `pos: {id: 物理位序}`——子集按**索引物理序**回排:基线 `_scan` 的候选序
|
|
20
|
+
就是快照物理序,等价性判据(flag 开/关逐位一致)要求取数面能复现它。
|
|
21
|
+
|
|
22
|
+
**为什么 nonfinite 单列**(不并入 untimed):基线里 NaN 时间戳节点并非「无时间」——
|
|
23
|
+
`_interval` 对 `(nan, nan)` 判非 None,`time_relation` 落到最后一档 `overlaps`,于是
|
|
24
|
+
NaN 节点**会**命中时间窗查询;`±inf` 则分别落 `after`/`before`(永不命中)。故 NaN 必须
|
|
25
|
+
进时间窗查询的候选面(`subset` 对 time 维一并带上 nonfinite——保守超集,逐条精确判定
|
|
26
|
+
仍在 `_scan` 侧),untimed 才与基线「跳过」语义对齐。
|
|
27
|
+
|
|
28
|
+
不适用条件:非 stg 读侧(`cg(op=read)` 的候选收敛走 `apply_retrieval_gates`,与
|
|
29
|
+
本索引不同维、不共用);跨进程共享(本索引是**进程内**派生结构,各进程自建)。
|
|
30
|
+
"""
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import bisect
|
|
34
|
+
import math
|
|
35
|
+
|
|
36
|
+
from . import trust
|
|
37
|
+
|
|
38
|
+
#: 不可哈希的 session/layer 值(脏盘面才可能出现的形态)统一归此哨兵键。
|
|
39
|
+
#: 任何**具体会话/层**查询都查不到它——与基线 `!=` 等值比较同语义(脏值恒不等)。
|
|
40
|
+
_UNHASHABLE = object()
|
|
41
|
+
|
|
42
|
+
#: 维缺省哨兵(`None` 是**合法取值**:session=None 即 unassigned 桶,不可与「未给该维」混同)。
|
|
43
|
+
MISSING = object()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# 生效条件:value 可 hash 时原样返回,hash 抛 TypeError(list/dict 等)时返回模块级哨兵 _UNHASHABLE。
|
|
47
|
+
def _hashable(value):
|
|
48
|
+
try:
|
|
49
|
+
hash(value)
|
|
50
|
+
except TypeError:
|
|
51
|
+
return _UNHASHABLE
|
|
52
|
+
return value
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# 生效条件:entry 为 dict 时以 trust.time_window_of(entry, "observed") 取观察轴两端点,两端均可判定则返回 float(起点),任一端不可判定(None)或 entry 非 dict 时返回 None;不做第二套解析、不猜测。
|
|
56
|
+
def time_of(entry):
|
|
57
|
+
"""条目 → 观察轴区间**起点**(epoch 秒);不可判定 → None。
|
|
58
|
+
|
|
59
|
+
与 `stg._interval` **同一单点**:`_scan` 把快照条目拼成 fm 后取到的区间,
|
|
60
|
+
与直接对条目取逐位一致——`_scan` 把 `time_window` 平铺进 `condition_space`
|
|
61
|
+
而 `time_window_of` 两种载体都读(见其「兼容两种载体形态」注释),
|
|
62
|
+
`temporal` 优先级与毫秒归一也同一份 `trust.parse_time`。
|
|
63
|
+
"""
|
|
64
|
+
if not isinstance(entry, dict):
|
|
65
|
+
return None
|
|
66
|
+
s, e = trust.time_window_of(entry, "observed")
|
|
67
|
+
if s is None or e is None:
|
|
68
|
+
return None
|
|
69
|
+
return float(s)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# 生效条件:index_obj 为含 dict 型 "nodes" 的快照(或顶层就是 dict)时返回按其**物理序**一次性构建的 StgIndex(O(N),逐条经 add 分类);"nodes" 缺失或非 dict 时返回 None(调用方据此回退并上报 reason,不静默)。
|
|
73
|
+
def build(index_obj):
|
|
74
|
+
"""快照 → 三张表(O(N));快照形态不可用返回 None。"""
|
|
75
|
+
nodes = index_obj.get("nodes") if isinstance(index_obj, dict) else None
|
|
76
|
+
if not isinstance(nodes, dict):
|
|
77
|
+
return None
|
|
78
|
+
ix = StgIndex(index_obj)
|
|
79
|
+
for nid in nodes: # 快照物理序(dict 插入序)→ pos 即此序
|
|
80
|
+
ix.add(nid, nodes[nid])
|
|
81
|
+
return ix
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# 生效条件:ix 为 StgIndex、nodes 为快照 nodes 映射时,逐项核对「三表恒为快照 keys 的划分」——pos 与快照键集双向相等、pos 序 == 快照物理序、by_session/by_layer 各桶两两不交且并集 == 键集、by_time ∪ nonfinite ∪ untimed 同上、by_time 升序且分类与条目实际 session/layer/时间逐条一致;返回不一致说明列表(空 = 不变量成立)。O(N),守卫/审计用,不在取数热路径。
|
|
85
|
+
def verify(ix, nodes):
|
|
86
|
+
"""划分不变量自检:返回不一致说明列表(空 = 完好)。"""
|
|
87
|
+
bad = []
|
|
88
|
+
if ix.broken:
|
|
89
|
+
bad.append("broken:增量维护期已记不一致标记(写钩异常)")
|
|
90
|
+
ids = set(nodes)
|
|
91
|
+
if set(ix.pos) != ids:
|
|
92
|
+
bad.append("pos 键集与快照不等:多 %d / 缺 %d"
|
|
93
|
+
% (len(set(ix.pos) - ids), len(ids - set(ix.pos))))
|
|
94
|
+
if sorted(ix.pos, key=ix.pos.get) != list(nodes):
|
|
95
|
+
bad.append("pos 序与快照物理序不一致(取数面无法复现基线候选序)")
|
|
96
|
+
if len(ix.pos) != len(nodes):
|
|
97
|
+
bad.append("len(pos)=%d ≠ len(nodes)=%d" % (len(ix.pos), len(nodes)))
|
|
98
|
+
for name, table in (("by_session", ix.by_session), ("by_layer", ix.by_layer)):
|
|
99
|
+
seen = []
|
|
100
|
+
for bucket, lst in table.items():
|
|
101
|
+
seen.extend(lst)
|
|
102
|
+
if len(set(lst)) != len(lst):
|
|
103
|
+
bad.append("%s[%r] 桶内重复 id" % (name, bucket))
|
|
104
|
+
if len(seen) != len(set(seen)):
|
|
105
|
+
bad.append("%s 有 id 落在多个桶(非划分)" % name)
|
|
106
|
+
if set(seen) != ids:
|
|
107
|
+
bad.append("%s 并集与快照键集不等(多 %d / 缺 %d)"
|
|
108
|
+
% (name, len(set(seen) - ids), len(ids - set(seen))))
|
|
109
|
+
timed = [nid for _t, nid in ix.by_time]
|
|
110
|
+
sess_sets = {k: set(v) for k, v in ix.by_session.items()}
|
|
111
|
+
layer_sets = {k: set(v) for k, v in ix.by_layer.items()}
|
|
112
|
+
union = set(timed) | set(ix.nonfinite) | set(ix.untimed)
|
|
113
|
+
if len(timed) != len(set(timed)) or len(set(ix.nonfinite)) != len(ix.nonfinite) \
|
|
114
|
+
or len(set(ix.untimed)) != len(ix.untimed):
|
|
115
|
+
bad.append("by_time/nonfinite/untimed 有重复 id")
|
|
116
|
+
if union != ids:
|
|
117
|
+
bad.append("by_time ∪ nonfinite ∪ untimed 与快照键集不等(多 %d / 缺 %d)"
|
|
118
|
+
% (len(union - ids), len(ids - union)))
|
|
119
|
+
if [t for t, _ in ix.by_time] != sorted(t for t, _ in ix.by_time):
|
|
120
|
+
bad.append("by_time 非升序")
|
|
121
|
+
for nid in ids:
|
|
122
|
+
e = nodes.get(nid)
|
|
123
|
+
sid, layer, bucket, _t0 = ix.cls.get(nid, (None, None, None, None))
|
|
124
|
+
if sid != _hashable((e or {}).get("session")):
|
|
125
|
+
bad.append("%s 的 by_session 分类与条目不符" % nid)
|
|
126
|
+
if layer != _hashable((e or {}).get("layer")):
|
|
127
|
+
bad.append("%s 的 by_layer 分类与条目不符" % nid)
|
|
128
|
+
t = time_of(e)
|
|
129
|
+
want = 0 if (t is not None and math.isfinite(t)) else (
|
|
130
|
+
1 if t is not None else 2)
|
|
131
|
+
if bucket != want:
|
|
132
|
+
bad.append("%s 的 by_time 分类与条目不符(t=%r)" % (nid, t))
|
|
133
|
+
# 「划分」不只要求并集相等,还要求 id 落在**其分类键对应的桶**里——
|
|
134
|
+
# 否则分桶键取错字段(如 by_layer 按 session 建)时并集仍相等、静默漏判。
|
|
135
|
+
if nid not in sess_sets.get(sid, ()):
|
|
136
|
+
bad.append("%s 不在其分类对应的 by_session 桶里" % nid)
|
|
137
|
+
if nid not in layer_sets.get(layer, ()):
|
|
138
|
+
bad.append("%s 不在其分类对应的 by_layer 桶里" % nid)
|
|
139
|
+
if bucket == 0 and nid not in timed:
|
|
140
|
+
bad.append("%s 应在 by_time 而不在" % nid)
|
|
141
|
+
if bucket == 1 and nid not in ix.nonfinite:
|
|
142
|
+
bad.append("%s 应在 nonfinite 而不在" % nid)
|
|
143
|
+
if bucket == 2 and nid not in ix.untimed:
|
|
144
|
+
bad.append("%s 应在 untimed 而不在" % nid)
|
|
145
|
+
return bad
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class StgIndex:
|
|
149
|
+
"""快照的结构倒排(三张表 + 物理位序 + 每节点分类记录)。
|
|
150
|
+
|
|
151
|
+
维护纪律:增删只经 `add` / `remove`(写侧单点),取数只经 `subset`(读侧单点)。
|
|
152
|
+
`cls[nid] = (sid, layer, bucket, t)` 是**增量摘除**的依据(不必回扫桶猜分类);
|
|
153
|
+
任一步异常 ⇒ `broken = True`(读侧据此回退全量并重建,绝不带着半张表取数)。
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
def __init__(self, index_obj):
|
|
157
|
+
self.index_obj = index_obj # 强引用:读侧按 `is` 判代际(防 id 复用)
|
|
158
|
+
self.by_session = {}
|
|
159
|
+
self.by_layer = {}
|
|
160
|
+
self.by_time = []
|
|
161
|
+
self.untimed = []
|
|
162
|
+
self.nonfinite = []
|
|
163
|
+
self.pos = {}
|
|
164
|
+
self.cls = {}
|
|
165
|
+
self.broken = False
|
|
166
|
+
self._next_pos = 0
|
|
167
|
+
|
|
168
|
+
# 生效条件:nid 已在 pos 中时先摘旧分类(**位序不变**——与 dict 原地覆写保序同语义),否则分配下一个位序;随后按 entry 的 session/layer/时间重新分类一次(同键即幂等);任一步异常置 broken=True(读侧回退重建)。
|
|
169
|
+
def add(self, nid, entry):
|
|
170
|
+
"""增量增/改:与 `dict.__setitem__` 同序语义(已有键保位序、新键落尾)。"""
|
|
171
|
+
try:
|
|
172
|
+
if nid in self.pos:
|
|
173
|
+
self._unclassify(nid) # 位序保留(dict 覆写不移动)
|
|
174
|
+
else:
|
|
175
|
+
self.pos[nid] = self._next_pos
|
|
176
|
+
self._next_pos += 1
|
|
177
|
+
self._classify(nid, entry)
|
|
178
|
+
except Exception: # noqa: BLE001 一致性存疑即标记,读侧回退
|
|
179
|
+
self.broken = True
|
|
180
|
+
|
|
181
|
+
# 生效条件:nid 不在 pos 中时无操作;在则摘分类并 pop 位序(位序**不回收**——后续新键恒得更大位序,与 dict 删后新增落尾一致);任一步异常置 broken=True。
|
|
182
|
+
def remove(self, nid):
|
|
183
|
+
"""增量删(不回收位序:新键恒落尾,与 dict 删除/追加语义同构)。"""
|
|
184
|
+
try:
|
|
185
|
+
if nid not in self.pos:
|
|
186
|
+
return
|
|
187
|
+
self._unclassify(nid)
|
|
188
|
+
self.pos.pop(nid, None)
|
|
189
|
+
except Exception: # noqa: BLE001
|
|
190
|
+
self.broken = True
|
|
191
|
+
|
|
192
|
+
def _classify(self, nid, entry):
|
|
193
|
+
e = entry if isinstance(entry, dict) else {}
|
|
194
|
+
sid, layer = _hashable(e.get("session")), _hashable(e.get("layer"))
|
|
195
|
+
t = time_of(e)
|
|
196
|
+
self.by_session.setdefault(sid, []).append(nid)
|
|
197
|
+
self.by_layer.setdefault(layer, []).append(nid)
|
|
198
|
+
if t is None:
|
|
199
|
+
bucket = 2
|
|
200
|
+
self.untimed.append(nid)
|
|
201
|
+
elif math.isfinite(t):
|
|
202
|
+
bucket = 0
|
|
203
|
+
bisect.insort(self.by_time, (t, nid))
|
|
204
|
+
else:
|
|
205
|
+
bucket = 1 # NaN/±inf:见模块 docstring「为什么单列」
|
|
206
|
+
self.nonfinite.append(nid)
|
|
207
|
+
self.cls[nid] = (sid, layer, bucket, t)
|
|
208
|
+
|
|
209
|
+
def _unclassify(self, nid):
|
|
210
|
+
sid, layer, bucket, t = self.cls.pop(nid)
|
|
211
|
+
lst = self.by_session[sid]
|
|
212
|
+
lst.remove(nid)
|
|
213
|
+
if not lst: # 空桶不保留(表规模读数如实)
|
|
214
|
+
self.by_session.pop(sid, None)
|
|
215
|
+
lst = self.by_layer[layer]
|
|
216
|
+
lst.remove(nid)
|
|
217
|
+
if not lst:
|
|
218
|
+
self.by_layer.pop(layer, None)
|
|
219
|
+
if bucket == 0:
|
|
220
|
+
# 二分定位而非全表连比:(t, nid) 在升序表内唯一,命中即该位;
|
|
221
|
+
# 不在该位 ⇒ 状态已不可信,抛错由上层置 broken(绝不摘错条目)
|
|
222
|
+
i = bisect.bisect_left(self.by_time, (t, nid))
|
|
223
|
+
if i >= len(self.by_time) or self.by_time[i] != (t, nid):
|
|
224
|
+
raise KeyError(nid)
|
|
225
|
+
del self.by_time[i]
|
|
226
|
+
elif bucket == 1:
|
|
227
|
+
self.nonfinite.remove(nid)
|
|
228
|
+
else:
|
|
229
|
+
self.untimed.remove(nid)
|
|
230
|
+
|
|
231
|
+
# 生效条件:lo <= hi 时返回 t ∈ [lo, hi] 的 id 列表(**时间序**,调用方按 pos 回排);lo > hi 返回空列表(区间为空——调用方在 inverted 查询上先回退,见 stg 侧)。
|
|
232
|
+
def range_ids(self, lo, hi):
|
|
233
|
+
"""`by_time` 区间截取(二分);返回时间序 id 列表。"""
|
|
234
|
+
if lo > hi:
|
|
235
|
+
return []
|
|
236
|
+
a = bisect.bisect_left(self.by_time, (lo,))
|
|
237
|
+
b = bisect.bisect_left(self.by_time, (hi,))
|
|
238
|
+
# 同 t 并列:把 t == hi 的尾巴一并纳入(不假设 id 字符上界,避免脆弱判据)
|
|
239
|
+
while b < len(self.by_time) and self.by_time[b][0] == hi:
|
|
240
|
+
b += 1
|
|
241
|
+
return [nid for _t, nid in self.by_time[a:b]]
|
|
242
|
+
|
|
243
|
+
# 生效条件:session/layer 为 MISSING 表示该维不参与(**注意 None 是合法维值**,不表示缺省),time_range 为 None 表示时间维不参与、否则为 (lo, hi);无任何参与维时返回 (None, ());否则返回 (候选 id 列表(**索引物理序**), 实际服务的维名元组)——任一维在表中无键即得空列表(与基线等值比较同语义,非回退)。
|
|
244
|
+
def subset(self, session=MISSING, layer=MISSING, time_range=None):
|
|
245
|
+
"""按维直取子集;返回 (ids | None, dims)。"""
|
|
246
|
+
dims, srcs = [], []
|
|
247
|
+
if session is not MISSING:
|
|
248
|
+
dims.append("session")
|
|
249
|
+
srcs.append(self.by_session.get(session, []))
|
|
250
|
+
if layer is not MISSING:
|
|
251
|
+
dims.append("layer")
|
|
252
|
+
srcs.append(self.by_layer.get(layer, []))
|
|
253
|
+
if time_range is not None:
|
|
254
|
+
dims.append("time")
|
|
255
|
+
lo, hi = time_range
|
|
256
|
+
# nonfinite 是保守超集(NaN 在基线上会命中时间窗,见模块 docstring)
|
|
257
|
+
srcs.append(self.range_ids(lo, hi) + list(self.nonfinite))
|
|
258
|
+
if not dims:
|
|
259
|
+
return None, ()
|
|
260
|
+
if len(srcs) > 1:
|
|
261
|
+
# 交集:三张表各自都是**物理序子序列**,除 time 外直接可用;
|
|
262
|
+
# time 先回排成物理序,再以最短表驱动(结果仍是物理序子序列)。
|
|
263
|
+
for i, d in enumerate(dims):
|
|
264
|
+
if d == "time":
|
|
265
|
+
srcs[i] = sorted(srcs[i], key=self.pos.__getitem__)
|
|
266
|
+
k = min(range(len(srcs)), key=lambda i: len(srcs[i]))
|
|
267
|
+
driver = srcs[k]
|
|
268
|
+
others = [set(srcs[i]) for i in range(len(srcs)) if i != k]
|
|
269
|
+
return ([nid for nid in driver if all(nid in o for o in others)],
|
|
270
|
+
tuple(dims))
|
|
271
|
+
ids = srcs[0]
|
|
272
|
+
if dims[0] == "time":
|
|
273
|
+
ids = sorted(ids, key=self.pos.__getitem__)
|
|
274
|
+
return list(ids), tuple(dims)
|
|
275
|
+
|
|
276
|
+
# 生效条件:恒返回表规模读数 dict——nodes=pos 条数、sessions/layers=两张表的**非空桶**数、timed=by_time 条数、untimed/nonfinite=两表条数;只读、无副作用。
|
|
277
|
+
def size(self):
|
|
278
|
+
"""表规模读数(meta 用)。"""
|
|
279
|
+
return {"nodes": len(self.pos), "sessions": len(self.by_session),
|
|
280
|
+
"layers": len(self.by_layer), "timed": len(self.by_time),
|
|
281
|
+
"untimed": len(self.untimed), "nonfinite": len(self.nonfinite)}
|
package/md_cg/sustain.py
CHANGED
|
@@ -70,6 +70,31 @@ DEFAULT_HEAL_INTERVAL = 300.0 # 自愈巡检 5min
|
|
|
70
70
|
DEFAULT_SCRUB_INTERVAL = 3600.0 # 记忆自净(抽查/去污染/校准)1h
|
|
71
71
|
DEFAULT_EVOLVE_INTERVAL = 7200.0 # 演化巡检(固化/重要性候选盘点)2h;只读
|
|
72
72
|
DEFAULT_TIDY_INTERVAL = 21600.0 # 整理巡检(contextual 同构组聚合)6h
|
|
73
|
+
|
|
74
|
+
# ---- 四档 `auto_*` 缺省的**单一真源**(P0-2,2026-10-01)--------------------
|
|
75
|
+
# 为什么要有这张表:同一组缺省此前在**三处**各写一份——op 路径
|
|
76
|
+
# (mcp_server.py 的 `_sustain_call` start 分支)、env 路径(`_start_sustain`)
|
|
77
|
+
# 与 `SustainLoop.__init__` 形参。三份必然漂移,且已经漂了:`auto_tidy` 在
|
|
78
|
+
# op 路径是 `False`、在 env 路径是 `"1"`(True)——同一个 `(root,name)` 走哪条
|
|
79
|
+
# 入口得到相反的整理语义,是**对外可见的缺省不一致**。
|
|
80
|
+
# 纪律:改缺省只改这里;调用点只许经 `auto_default` / `auto_from_env` /
|
|
81
|
+
# `auto_from_args` 读取,**不得再写第二处字面量**(守卫 test_auto_defaults.py 钉死)。
|
|
82
|
+
#
|
|
83
|
+
# ⚠ 本轮**对外可见的缺省变更**(P0-2 裁决值 = 开):`auto_tidy` 由 op 路径原
|
|
84
|
+
# 字面量 `False` 收敛为 `True`,取 env 路径(生产路径:常驻 serve 自启)既有值
|
|
85
|
+
# ——该动作确定性、永不删除节点、可逆可审计(见 `_tick_tidy` 说明),op 路径的
|
|
86
|
+
# `False` 是唯一错位项。**opt-out:`MDCG_AUTO_TIDY=0`(env 路径)/显式传
|
|
87
|
+
# `auto_tidy=false`(op 路径——显式入参仍优先于本表)。**
|
|
88
|
+
AUTO_DEFAULTS = {"auto_heal": True, "auto_scrub": False,
|
|
89
|
+
"auto_evolve": False, "auto_tidy": True}
|
|
90
|
+
#: 各 `auto_*` 的 env 覆盖键(env 路径入口照此读;即各档的 opt-out 名)。
|
|
91
|
+
AUTO_ENVS = {"auto_heal": "MDCG_SUSTAIN_AUTOHEAL",
|
|
92
|
+
"auto_scrub": "MDCG_AUTO_SCRUB",
|
|
93
|
+
"auto_evolve": "MDCG_AUTO_EVOLVE",
|
|
94
|
+
"auto_tidy": "MDCG_AUTO_TIDY"}
|
|
95
|
+
#: 关断字面量——与两入口既有口径逐字一致的三写法(`0` / `false` / `False`)。
|
|
96
|
+
AUTO_OFF_VALUES = ("0", "false", "False")
|
|
97
|
+
|
|
73
98
|
DEFAULT_WARN_FACTOR = 2.5 # 2.5× 心跳间隔 → 警告
|
|
74
99
|
DEFAULT_DEAD_FACTOR = 3.5 # 3.5× → 失联
|
|
75
100
|
DEFAULT_WORKING_FACTOR = 2.0 # 任务执行中阈值 ×2
|
|
@@ -78,6 +103,39 @@ ACCESS_LOG_COMPACT_LINES = 500 # 访问日志超过该行数即折叠(否
|
|
|
78
103
|
_POLL = 0.2 # 循环轮询步长(常驻进程 CPU 可忽略)
|
|
79
104
|
|
|
80
105
|
|
|
106
|
+
# 生效条件:name 为 AUTO_DEFAULTS 的键时返回该档缺省的 bool(真源表取值,无副作用);键不存在时抛 KeyError(不做静默回落——拼错档名即为编程错误)。
|
|
107
|
+
def auto_default(name: str) -> bool:
|
|
108
|
+
"""`auto_*` 缺省的真源读取(无环境、无入参)。"""
|
|
109
|
+
return bool(AUTO_DEFAULTS[name])
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# 生效条件:name 为 AUTO_DEFAULTS 的键时,从 environ(缺省 os.environ)按 AUTO_ENVS[name] 取名取值,缺键时回落「真源缺省对应的字面量」(真值→"1"、假值→"0");取值经 str() 后不属于 AUTO_OFF_VALUES 即为真。返回 bool。
|
|
113
|
+
def auto_from_env(name: str, environ=None) -> bool:
|
|
114
|
+
"""env 路径(常驻 serve 自启)的 `auto_*` 读取器。
|
|
115
|
+
|
|
116
|
+
与改动前的逐处字面量**同义**:`os.environ.get(<键>, <默认>) not in
|
|
117
|
+
("0", "false", "False")`——默认字面量由真源表推出,不再各写一份。
|
|
118
|
+
"""
|
|
119
|
+
env = os.environ if environ is None else environ
|
|
120
|
+
return str(env.get(AUTO_ENVS[name],
|
|
121
|
+
"1" if AUTO_DEFAULTS[name] else "0")) not in AUTO_OFF_VALUES
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# 生效条件:args 为 dict 且含 name 键时返回 bool(args[name])(显式传 None 亦为 False——与改动前 `bool(a.get(name, <默认>))` 逐字同义);args 非 dict 或缺该键时回落 auto_default(name)。
|
|
125
|
+
def auto_from_args(name: str, args, environ=None) -> bool:
|
|
126
|
+
"""op 路径(工具面 `sustain action=start`)的 `auto_*` 读取器。
|
|
127
|
+
|
|
128
|
+
判据是**键在不在**而不是值真假:`{"auto_tidy": False}` 与
|
|
129
|
+
`{"auto_tidy": None}` 都按「显式给了」处理(前者关、后者按 bool(None)=False
|
|
130
|
+
关),缺键才回落真源缺省——与改动前 `a.get(name, default)` 的语义一字不差。
|
|
131
|
+
`environ` 仅为签名对齐 `auto_from_env`(op 路径不读 env;保留位以免调用点
|
|
132
|
+
两边形参不一致)。
|
|
133
|
+
"""
|
|
134
|
+
if isinstance(args, dict) and name in args:
|
|
135
|
+
return bool(args[name])
|
|
136
|
+
return auto_default(name)
|
|
137
|
+
|
|
138
|
+
|
|
81
139
|
# --------------------------------------------------------------------------
|
|
82
140
|
# 心跳
|
|
83
141
|
# --------------------------------------------------------------------------
|
|
@@ -944,7 +1002,7 @@ def watermarks(cg) -> dict:
|
|
|
944
1002
|
# 常驻循环
|
|
945
1003
|
# --------------------------------------------------------------------------
|
|
946
1004
|
|
|
947
|
-
# 生效条件:传入 cg 即构造实例并把 self.cg 指向它,name/beat_interval/heal_interval/auto_heal/scrub_interval/auto_scrub/evolve_interval/auto_evolve/tidy_interval/auto_tidy 用各默认值(DEFAULT_* 与
|
|
1005
|
+
# 生效条件:传入 cg 即构造实例并把 self.cg 指向它,name/beat_interval/heal_interval/auto_heal/scrub_interval/auto_scrub/evolve_interval/auto_evolve/tidy_interval/auto_tidy 用各默认值(DEFAULT_* 与 AUTO_DEFAULTS 真源表)经 float()/bool() 落为 self 属性,ledger 为假值(默认 None)时回落 SessionLedger(cg.root),d 经 net_dir(d) 赋值,其余运行态字段初始化为 False/None/空列表/空 Event/Lock
|
|
948
1006
|
class SustainLoop:
|
|
949
1007
|
"""常驻自维持循环:后台线程周期心跳 + 周期巡检 + 必要时自愈。
|
|
950
1008
|
|
|
@@ -952,18 +1010,31 @@ class SustainLoop:
|
|
|
952
1010
|
让对端立刻看到「正常下线」而不是「失联」。
|
|
953
1011
|
"""
|
|
954
1012
|
|
|
955
|
-
# 生效条件:传入 cg 时按 name 与各 DEFAULT_*
|
|
1013
|
+
# 生效条件:传入 cg 时按 name 与各 DEFAULT_* / AUTO_DEFAULTS 真源表默认值初始化——self.d=net_dir(d)(d 假值时回落 MDCG_SUSTAIN_DIR/~/ .mdcg/sustain)、self.ledger=ledger or SessionLedger(cg.root)(ledger 假值时新建),beat/heal/scrub/evolve/tidy 间隔 float() 化、auto_heal/auto_scrub/auto_evolve/auto_tidy bool() 化后存为实例属性;
|
|
956
1014
|
def __init__(self, cg, name: str = "md_cg", *,
|
|
957
1015
|
beat_interval: float = DEFAULT_BEAT_INTERVAL,
|
|
958
1016
|
heal_interval: float = DEFAULT_HEAL_INTERVAL,
|
|
959
|
-
|
|
1017
|
+
# 四档 `auto_*` 缺省取自**单一真源**(P0-2):本形参默认值与
|
|
1018
|
+
# 两个入口读取的是同一张表,`SustainLoop(cg)` 与
|
|
1019
|
+
# `sustain action=start` / `_start_sustain` 三面同值。
|
|
1020
|
+
auto_heal: bool = AUTO_DEFAULTS["auto_heal"], d: str = None,
|
|
960
1021
|
ledger: SessionLedger = None,
|
|
961
1022
|
scrub_interval: float = DEFAULT_SCRUB_INTERVAL,
|
|
962
|
-
auto_scrub: bool =
|
|
1023
|
+
auto_scrub: bool = AUTO_DEFAULTS["auto_scrub"],
|
|
963
1024
|
evolve_interval: float = DEFAULT_EVOLVE_INTERVAL,
|
|
964
|
-
auto_evolve: bool =
|
|
1025
|
+
auto_evolve: bool = AUTO_DEFAULTS["auto_evolve"],
|
|
965
1026
|
tidy_interval: float = DEFAULT_TIDY_INTERVAL,
|
|
966
|
-
auto_tidy: bool =
|
|
1027
|
+
auto_tidy: bool = AUTO_DEFAULTS["auto_tidy"],
|
|
1028
|
+
# 第六档:睡眠周期(§三 九步 / §4.7)。四个缺省一律取自
|
|
1029
|
+
# `md_cg/sleep.py` 的 **env 表单一真源**(SLEEP_ENV_DEFAULTS +
|
|
1030
|
+
# sleep_env 族读取器)——本处**不写第二份缺省字面量**;两个
|
|
1031
|
+
# 入口(`_start_sustain` / op 路径)同样只经那些读取器。
|
|
1032
|
+
sleep_interval: float = None,
|
|
1033
|
+
auto_sleep: bool = None,
|
|
1034
|
+
sleep_merge: str = None,
|
|
1035
|
+
sleep_window: str = None,
|
|
1036
|
+
sleep_scrub_apply: bool = None):
|
|
1037
|
+
from . import sleep as _sleep
|
|
967
1038
|
self.cg = cg
|
|
968
1039
|
self.name = name
|
|
969
1040
|
self.beat_interval = float(beat_interval)
|
|
@@ -977,6 +1048,22 @@ class SustainLoop:
|
|
|
977
1048
|
self.auto_evolve = bool(auto_evolve)
|
|
978
1049
|
self.tidy_interval = float(tidy_interval)
|
|
979
1050
|
self.auto_tidy = bool(auto_tidy)
|
|
1051
|
+
# 第六档睡眠周期:值全来自 sleep 模块的真源读取器(缺省见 §4.7 表)。
|
|
1052
|
+
self.sleep_interval = float(_sleep.sleep_interval()
|
|
1053
|
+
if sleep_interval is None
|
|
1054
|
+
else sleep_interval)
|
|
1055
|
+
self.auto_sleep = bool(_sleep.sleep_enabled() if auto_sleep is None
|
|
1056
|
+
else auto_sleep)
|
|
1057
|
+
self.sleep_merge = (_sleep.sleep_merge_mode() if sleep_merge is None
|
|
1058
|
+
else str(sleep_merge))
|
|
1059
|
+
self.sleep_window = (_sleep.sleep_window() if sleep_window is None
|
|
1060
|
+
else str(sleep_window))
|
|
1061
|
+
self.sleep_scrub_apply = bool(_sleep.sleep_scrub_apply()
|
|
1062
|
+
if sleep_scrub_apply is None
|
|
1063
|
+
else sleep_scrub_apply)
|
|
1064
|
+
self.last_sleep = None
|
|
1065
|
+
self.sleeps = []
|
|
1066
|
+
self.sleep_round = 0
|
|
980
1067
|
self.task_running = False
|
|
981
1068
|
self.beats = 0
|
|
982
1069
|
self.last_beat = None
|
|
@@ -1032,13 +1119,14 @@ class SustainLoop:
|
|
|
1032
1119
|
clear_stamp(self.name, self.d)
|
|
1033
1120
|
return self
|
|
1034
1121
|
|
|
1035
|
-
# 生效条件:self._stop 未置位期间轮询,按 beat_interval/heal_interval/scrub_interval/evolve_interval/tidy_interval 到期分别执行 beat 与 _tick_heal/_tick_scrub/_tick_evolve/_tick_tidy,各 tick 抛出的异常被吞掉不中断循环,末尾以 _stop.wait(_POLL) 休眠;
|
|
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) 休眠;
|
|
1036
1123
|
def _run(self):
|
|
1037
1124
|
next_beat = time.time() + self.beat_interval
|
|
1038
1125
|
next_heal = time.time() + self.heal_interval
|
|
1039
1126
|
next_scrub = time.time() + self.scrub_interval
|
|
1040
1127
|
next_evolve = time.time() + self.evolve_interval
|
|
1041
1128
|
next_tidy = time.time() + self.tidy_interval
|
|
1129
|
+
next_sleep = time.time() + self.sleep_interval
|
|
1042
1130
|
while not self._stop.is_set():
|
|
1043
1131
|
now = time.time()
|
|
1044
1132
|
if now >= next_beat:
|
|
@@ -1071,13 +1159,20 @@ class SustainLoop:
|
|
|
1071
1159
|
except Exception:
|
|
1072
1160
|
pass # 整理巡检失败不中断常驻
|
|
1073
1161
|
next_tidy = now + self.tidy_interval
|
|
1162
|
+
if now >= next_sleep:
|
|
1163
|
+
try:
|
|
1164
|
+
self._tick_sleep()
|
|
1165
|
+
except Exception:
|
|
1166
|
+
pass # 睡眠周期失败不中断常驻
|
|
1167
|
+
next_sleep = now + self.sleep_interval
|
|
1074
1168
|
self._stop.wait(_POLL)
|
|
1075
1169
|
|
|
1076
|
-
# 生效条件:以 apply=self.auto_tidy 调 writelimit.tidy_contextual(self.cg, actor="sustain_tidy"),把 t/scanned/groups/members/applied_count/auto_tidy 记入 self.last_tidy 与 tidys(仅保留最近 20 条),随后调 _tick_conformance();auto_tidy
|
|
1170
|
+
# 生效条件:以 apply=self.auto_tidy 调 writelimit.tidy_contextual(self.cg, actor="sustain_tidy"),把 t/scanned/groups/members/applied_count/auto_tidy 记入 self.last_tidy 与 tidys(仅保留最近 20 条),随后调 _tick_conformance();auto_tidy 为假时只盘点不落盘;
|
|
1077
1171
|
def _tick_tidy(self):
|
|
1078
1172
|
"""整理巡检(contextual 流水治理·读侧):同构组聚合 + 成员降权。
|
|
1079
1173
|
|
|
1080
|
-
确定性动作、永不删节点;`auto_tidy
|
|
1174
|
+
确定性动作、永不删节点;`auto_tidy` 为假时只盘点不落盘。缺省值取自
|
|
1175
|
+
模块级真源 `AUTO_DEFAULTS`(P0-2;缺省为 True,opt-out 见该表注释)。
|
|
1081
1176
|
治理对象:单日批次流水(「批次247收官记忆」×163 那类同模板写入)
|
|
1082
1177
|
—— 写入侧限流(writelimit.check)拦增量,本巡检收敛存量。
|
|
1083
1178
|
"""
|
|
@@ -1109,6 +1204,41 @@ class SustainLoop:
|
|
|
1109
1204
|
self.last_conformance = {"ok": False, "verdict": "BLINDSPOT",
|
|
1110
1205
|
"error": f"{type(e).__name__}: {e}"}
|
|
1111
1206
|
|
|
1207
|
+
# 生效条件:self.sleep_round 自增 1 后以 enabled=self.auto_sleep / merge_mode=self.sleep_merge / scrub_apply=self.sleep_scrub_apply / window=self.sleep_window / round_index=self.sleep_round 调 sleep.run_cycle(self.cg),把 t/batch/round/candidates/merged/skipped/conflicts/九步名与其 skipped 明细/auto_sleep/merge_mode 记入 last_sleep 与 sleeps(保留最近 20 条);auto_sleep 为假时 run_cycle 只记账不迭代;
|
|
1208
|
+
def _tick_sleep(self):
|
|
1209
|
+
"""睡眠周期(第六档 tick):§3.1 九步显式化 + §4.4 副本迭代与周期合并。
|
|
1210
|
+
|
|
1211
|
+
**只在副本上迭代**(物化 → 影子迭代 → 对账四闸 → 语义重放 + git 合并),
|
|
1212
|
+
主库真源面在非合并阶段逐字节不变。四个开关全取 `md_cg/sleep.py` 的 §4.7
|
|
1213
|
+
env 表真源:`MDCG_SLEEP`(总开关,缺省开)、`MDCG_SLEEP_MERGE`(缺省
|
|
1214
|
+
auto=自动走四阶段,冲突仍挂起)、`MDCG_SLEEP_WINDOW`(缺省 23:00-07:00,
|
|
1215
|
+
**窗口外只记账不迭代**)、`MDCG_SLEEP_SCRUB_APPLY`(缺省 **关**——第④步
|
|
1216
|
+
缺省只在副本上盘点、不落盘)。
|
|
1217
|
+
|
|
1218
|
+
⑤权重刷新与衰减 / ⑥索引重建两步本轮是**显式 no-op 占位**(台账里标
|
|
1219
|
+
`skipped: "未接线"`),故本轮**不动检索读数**。
|
|
1220
|
+
"""
|
|
1221
|
+
from . import sleep as _sleep
|
|
1222
|
+
self.sleep_round += 1
|
|
1223
|
+
r = _sleep.run_cycle(self.cg, enabled=self.auto_sleep,
|
|
1224
|
+
merge_mode=self.sleep_merge,
|
|
1225
|
+
scrub_apply=self.sleep_scrub_apply,
|
|
1226
|
+
window=self.sleep_window,
|
|
1227
|
+
round_index=self.sleep_round)
|
|
1228
|
+
steps = list(r.get("steps") or [])
|
|
1229
|
+
rec = {"t": r.get("t"), "batch": r.get("batch"), "round": r.get("round"),
|
|
1230
|
+
"candidates": r.get("candidates"),
|
|
1231
|
+
"merged": r.get("merged"), "skipped": r.get("skipped"),
|
|
1232
|
+
"conflicts": r.get("conflicts"),
|
|
1233
|
+
"steps": [s.get("step") for s in steps],
|
|
1234
|
+
"steps_skipped": ["%s:%s" % (s.get("step"), s.get("skipped"))
|
|
1235
|
+
for s in steps if s.get("skipped")],
|
|
1236
|
+
"auto_sleep": self.auto_sleep, "merge_mode": self.sleep_merge}
|
|
1237
|
+
self.last_sleep = rec
|
|
1238
|
+
with self._lock:
|
|
1239
|
+
self.sleeps.append(rec)
|
|
1240
|
+
self.sleeps = self.sleeps[-20:]
|
|
1241
|
+
|
|
1112
1242
|
# 生效条件:恒以 evolution_candidates(self.cg) 只读盘点并记入 last_evolve 与 evolves(保留最近 20 条);仅当 self.auto_evolve 为真且 ev["importance_drift"]["n"] 为真时才额外执行 weights.recalc(self.cg, apply=True, actor="sustain_evolve"),其异常写入 rec["applied"];
|
|
1113
1243
|
def _tick_evolve(self):
|
|
1114
1244
|
"""演化巡检(G7):盘点固化/重要性候选 —— 让「有能力」变成「有驱动」。
|
|
@@ -1206,6 +1336,13 @@ class SustainLoop:
|
|
|
1206
1336
|
"evolves": self.evolves[-5:],
|
|
1207
1337
|
"last_tidy": self.last_tidy,
|
|
1208
1338
|
"tidys": self.tidys[-5:],
|
|
1339
|
+
"sleep_interval": self.sleep_interval,
|
|
1340
|
+
"auto_sleep": self.auto_sleep,
|
|
1341
|
+
"sleep_merge": self.sleep_merge,
|
|
1342
|
+
"sleep_window": self.sleep_window,
|
|
1343
|
+
"sleep_scrub_apply": self.sleep_scrub_apply,
|
|
1344
|
+
"last_sleep": self.last_sleep,
|
|
1345
|
+
"sleeps": self.sleeps[-5:],
|
|
1209
1346
|
"last_conformance": self.last_conformance,
|
|
1210
1347
|
"peers": peers(self.d),
|
|
1211
1348
|
"sessions": self.ledger.summary()}
|