@furongjun1999/dsh-memory 0.4.7 → 0.4.8
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 +92 -37
- package/codebuddy/CODEBUDDY.md +195 -185
- package/docs/Pi/345/217/257/345/255/246/344/271/240/344/274/230/347/202/271_/347/201/265/346/236/242/345/244/247/350/204/221/346/224/271/350/277/233/344/272/244/346/216/245_20260915.md +144 -0
- package/docs/README.md +111 -0
- package/docs/discipline/harnesses.yaml +215 -152
- package/docs/discipline/templates/full.md.tmpl +60 -58
- package/docs/eval//347/254/254/344/270/211/346/226/271/351/252/214/350/257/201/346/212/245/345/221/212_LoCoMo_/347/201/265/346/236/242_.md +127 -0
- package/docs/eval//347/254/254/344/270/211/346/226/271/351/252/214/350/257/201/346/212/245/345/221/212_LoCoMo_/347/201/265/346/236/242_.png +0 -0
- package/docs/eval//347/254/254/344/270/211/346/226/271/351/252/214/350/257/201/346/212/245/345/221/212_/347/201/265/346/236/242_vs_dejavu_/347/273/237/344/270/200/350/257/204/345/210/206_v7.md +202 -0
- package/docs/experiments/m4-role-probe/census.py +86 -0
- package/docs/experiments/m4-role-probe/probe.py +89 -0
- package/docs/experiments/mapped_confidence/bench_mapped_conf.py +216 -0
- package/docs/experiments/mapped_confidence/post_check.py +75 -0
- package/docs/experiments/mapped_confidence/repro_thirdparty_tol.py +83 -0
- package/docs/experiments/mapped_confidence/result_mapped_conf.json +215 -0
- package/docs/{ → hive/}/350/234/202/345/267/242/345/267/245/344/275/234/350/256/260/345/277/206_/351/241/271/347/233/256/350/256/241/345/210/222.md +14 -2
- package/docs/{README → mdcg/README}/350/257/246/347/273/206/347/211/210_v0.4.5.md +9 -9
- package/docs/{ → mdcg/}/344/270/273/344/273/243/347/220/206/345/255/220/344/273/243/347/220/206/350/256/260/345/277/206/346/236/266/346/236/204/350/256/276/350/256/241.md +1 -1
- 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 +261 -0
- package/docs/{ → mdcg/}/345/215/225/345/205/203/350/207/252/346/210/221/351/224/232/347/202/271_/347/263/273/347/273/237/346/217/220/347/244/272/350/257/215/346/240/207/345/207/206_v0.3.md +1 -1
- package/docs/mdcg//345/255/220/344/273/243/347/220/206/351/205/215/347/275/256/346/240/207/345/207/206_v0.4.md +148 -0
- package/docs/{ → mdcg/}/346/272/220/347/240/201/347/272/247/346/236/266/346/236/204/345/256/241/350/256/241_GPT/346/211/271/350/257/204/345/257/271/347/205/247_v1.0.md +2 -2
- package/docs/{ → mdcg/}/347/201/265/346/236/242/344/270/211/345/261/202/346/213/206/345/210/206/350/247/204/345/210/222_v0.1.md +181 -181
- package/docs/{ → mdcg/}/347/274/272/345/217/243/345/215/225_P0/346/224/266/345/217/243_v0.1.md +2 -2
- package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276_G4-G8/347/274/272/345/217/243/350/243/201/345/256/232/345/215/225_v0.1.md +1 -1
- package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276_/347/264/242/345/274/225/344/270/216/345/267/245/347/250/213/350/247/204/350/214/203/345/214/226_/350/256/241/345/210/222_v0.1.md +4 -4
- package/docs/{ → mdcg/}/350/256/260/345/277/206/346/223/215/344/275/234/347/263/273/347/273/237_MdCGOS/344/270/216MCP/346/216/245/345/205/245_v0.1.md +2 -2
- package/docs/plans/GridWorld/346/234/200/345/260/217/351/227/255/347/216/257/350/247/204/346/240/274_v0.1.md +176 -0
- package/docs/{ → swarm/}/350/234/202/347/276/244/344/272/222/350/201/224_v0.1.md +3 -3
- package/docs/swarm//350/234/202/347/276/244/345/220/214/351/224/231/346/243/200/346/265/213/345/256/236/351/252/214/345/215/217/350/256/256_v0.1.md +242 -0
- package/docs/theory//345/215/225/347/272/277/347/250/213/344/270/216/346/263/250/346/204/217/345/212/233/351/233/206/344/270/255_/346/227/240/344/272/211/350/256/256/347/220/206/350/256/272/346/226/207/346/241/243_v1.0.md +129 -0
- package/docs/theory//345/271/266/345/217/221/345/277/205/347/204/266/346/200/247/347/220/206/350/256/272_v0.2.md +134 -0
- package/docs/theory//346/246/202/345/277/265/345/210/206/345/261/202/345/257/271/351/275/220/350/241/250_v0.1.md +145 -0
- package/docs/{ → theory/}/347/220/206/350/256/272_/346/234/272/345/210/266_/344/273/243/347/240/201_/345/256/236/351/252/214_/347/274/272/345/217/243/347/237/251/351/230/265_v0.1.md +3 -3
- package/docs/{ → theory/}/347/220/206/350/256/272/344/273/223/346/213/206/345/210/206/344/270/216/347/231/275/347/256/261/347/237/245/350/257/206/345/272/223/345/206/205/350/277/201_v0.1.md +1 -1
- package/docs/theory//350/256/244/347/237/245/344/273/243/347/220/206/344/270/216/346/224/266/346/225/233/347/273/223/346/236/204_/346/235/241/344/273/266/350/256/272/351/207/215/346/236/204_v0.1.md +221 -0
- package/docs/world_model//344/270/226/347/225/214/346/250/241/345/236/213_/347/245/236/347/273/217/347/275/221/347/273/234/345/272/225/345/261/202/346/236/266/346/236/204_v1.0.md +237 -0
- package/docs//345/255/230/347/256/227/344/270/200/344/275/223/346/236/266/346/236/204/345/255/246/344/271/240_/345/244/226/351/203/250/347/220/206/350/256/272/345/257/271/347/205/247/344/270/216/350/267/257/347/272/277/344/272/244/346/216/245_20260916.md +98 -0
- package/docs//345/255/230/347/256/227/344/270/200/344/275/223/347/245/236/347/273/217/347/275/221/347/273/234/346/236/266/346/236/204_/346/200/273/347/272/262/344/270/216/344/272/244/346/216/245_20260916.md +109 -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 +34 -6
- package/docs//347/231/275/347/256/261/345/214/226/347/272/262/351/242/206_/344/270/211/351/241/271/347/233/256/345/255/246/344/271/240/346/200/273/346/236/266/346/236/204/344/270/216Ghidra/344/272/244/346/216/245_20260916.md +104 -0
- package/docs//350/256/260/345/277/206/350/257/204/345/256/241/347/263/273/347/273/237_/347/253/213/351/241/271/350/256/276/350/256/241/344/270/216/346/226/275/345/267/245/344/272/244/346/216/245_20260915.md +183 -0
- package/dsh/README.md +1 -1
- package/lib/hooks.d.ts +5 -0
- package/lib/hooks.js +10 -1
- package/lib/lib/prompt_safety.d.ts +48 -0
- package/lib/lib/prompt_safety.js +62 -0
- package/md_cg/audit.py +76 -2
- package/md_cg/bench_membench.py +10 -5
- package/md_cg/bench_progressive.py +48 -9
- package/md_cg/branches.py +275 -0
- package/md_cg/ccgc.py +884 -0
- package/md_cg/conformance.py +662 -0
- package/md_cg/consistency.py +1 -1
- package/md_cg/corpus.py +1 -1
- package/md_cg/eval_common.py +6 -5
- package/md_cg/forgetting.py +11 -3
- package/md_cg/fsutil.py +63 -1
- package/md_cg/identity.py +2 -2
- package/md_cg/insight.py +2 -2
- package/md_cg/lifecycle.py +262 -0
- package/md_cg/mcp_server.py +547 -140
- package/md_cg/mdcg.py +206 -18
- package/md_cg/mdcos.py +512 -50
- package/md_cg/metacognition.py +2 -2
- package/md_cg/mreview/__init__.py +25 -0
- package/md_cg/mreview/__main__.py +107 -0
- package/md_cg/mreview/bundle.py +170 -0
- package/md_cg/mreview/candidates.py +253 -0
- package/md_cg/mreview/govern.py +674 -0
- package/md_cg/mreview/locate.py +905 -0
- package/md_cg/mreview/pipeline.py +701 -0
- package/md_cg/mreview/rules/duplication.json +21 -0
- package/md_cg/mreview/rules/field_coverage.json +54 -0
- package/md_cg/mreview/rules/source_license.json +21 -0
- package/md_cg/mreview/rules/template_flow.json +21 -0
- package/md_cg/mreview/ruleset.py +238 -0
- package/md_cg/nodefile.py +38 -0
- package/md_cg/predict.py +24 -6
- package/md_cg/protect.py +4 -4
- package/md_cg/refindex.py +41 -2
- package/md_cg/scrub.py +5 -1
- package/md_cg/self_state.py +1 -1
- package/md_cg/semantic/en_normalizer.py +85 -25
- package/md_cg/sustain.py +18 -0
- package/md_cg/tasks.py +447 -0
- package/md_cg/test_action_derive.py +203 -0
- package/md_cg/test_audit_rotate.py +270 -0
- package/md_cg/test_branches.py +249 -0
- package/md_cg/test_ccg_perturb.py +1 -1
- package/md_cg/test_ccgc.py +423 -0
- package/md_cg/test_conformance.py +343 -0
- package/md_cg/test_en_pipeline.py +24 -0
- package/md_cg/test_health_scale.py +173 -0
- package/md_cg/test_identity_attribution.py +6 -0
- package/md_cg/test_index_durability.py +224 -0
- package/md_cg/test_lifecycle.py +309 -0
- package/md_cg/test_mr_m1.py +679 -0
- package/md_cg/test_mr_m2.py +587 -0
- package/md_cg/test_mr_m3.py +703 -0
- package/md_cg/test_mr_m4.py +485 -0
- package/md_cg/test_p1.py +1 -1
- package/md_cg/test_p11_consistency.py +1 -1
- package/md_cg/test_p12_metacognition.py +2 -2
- package/md_cg/test_p16_self_state.py +1 -1
- package/md_cg/test_p26_refindex.py +1 -1
- package/md_cg/test_p27_docindex.py +22 -2
- package/md_cg/test_p28_refcheck.py +1 -1
- package/md_cg/test_p29_session_ingest_export.py +1 -1
- package/md_cg/test_p2_mcp.py +7 -1
- package/md_cg/test_p38_contextualize.py +1 -1
- package/md_cg/test_p39_vision_evidence.py +1 -1
- package/md_cg/test_p40_refine_worklist.py +1 -1
- package/md_cg/test_p41_evolve_patrol.py +1 -1
- package/md_cg/test_p42_provenance.py +1 -1
- package/md_cg/test_p43_pooling.py +41 -13
- package/md_cg/test_read_clip.py +137 -0
- package/md_cg/test_review_conformance.py +310 -0
- package/md_cg/test_tasks.py +409 -0
- package/md_cg/test_tool_face.py +189 -0
- package/md_cg/test_twophase.py +286 -0
- package/md_cg/test_writepipe.py +210 -0
- package/md_cg/theory.py +1 -1
- package/md_cg/tokens.py +95 -5
- package/md_cg/tool_face.py +250 -0
- package/md_cg/twophase.py +221 -0
- package/md_cg/units.py +546 -0
- package/md_cg/vision_evidence.py +1 -1
- package/md_cg/weights.py +1 -1
- package/md_cg/whitebox.py +1 -1
- package/md_cg/whitebox_kb/data/verify_cache.json +28 -0
- package/md_cg/whitebox_kb/data/verify_savings.jsonl +46 -0
- package/md_cg/whitebox_kb/wisdom/audit_log/chain_heat.json +10 -10
- package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db-shm +0 -0
- package/md_cg/whitebox_kb/wisdom/wisdom-book-cloud.db-wal +0 -0
- package/md_cg/writelimit.py +19 -3
- package/md_cg/writepipe.py +372 -0
- package/package.json +1 -1
- package/src/hooks.ts +10 -1
- package/src/lib/prompt_safety.ts +62 -0
- package/zcode/AGENTS.md +195 -185
- package/docs//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +0 -221
- /package/docs/{AGI → eval/AGI}/344/270/203/347/273/264/350/257/204/345/210/206/346/212/245/345/221/212_md_cg_v1.0.md" +0 -0
- /package/docs/{AGI → eval/AGI}/344/270/203/347/273/264/350/257/204/345/210/206/346/212/245/345/221/212_md_cg_v2.0.md" +0 -0
- /package/docs/{ → eval/}/345/256/236/351/252/214/346/226/271/346/241/210_/345/255/246/344/271/240/351/227/255/347/216/257AB/344/270/216/346/250/252/350/257/204_v1.0.md" +0 -0
- /package/docs/{ → eval/}/346/250/252/350/257/204_/345/205/255/345/256/266100/351/242/230/344/270/255/350/213/261/345/217/214/346/237/245_v1.0.md" +0 -0
- /package/docs/{guardrail-charter.md → mdcg/guardrail-charter.md} +0 -0
- /package/docs/{lingshu_tutorial.html → mdcg/lingshu_tutorial.html} +0 -0
- /package/docs/{memory-assessment.html → mdcg/memory-assessment.html} +0 -0
- /package/docs/{memory_score.html → mdcg/memory_score.html} +0 -0
- /package/docs/{memory_score.png → mdcg/memory_score.png} +0 -0
- /package/docs/{release_v0.3.0.md → mdcg/release_v0.3.0.md} +0 -0
- /package/docs/{release_v0.4.5.md → mdcg/release_v0.4.5.md} +0 -0
- /package/docs/{tool_table_v0.3.0.md → mdcg/tool_table_v0.3.0.md} +0 -0
- /package/docs/{ → mdcg/}/344/273/244/347/211/214/344/270/216/350/247/222/350/211/262/346/235/203/350/201/214/345/210/206/347/246/273_v0.1.md" +0 -0
- /package/docs/{ → mdcg/}/347/201/265/346/236/24282/345/267/245/345/205/267_/345/212/237/350/203/275/346/225/264/347/220/206/344/270/216/350/277/201/347/247/273/346/230/240/345/260/204_v0.1.md" +0 -0
- /package/docs/{ → mdcg/}/347/201/265/346/236/242MCP/345/267/245/345/205/267/346/200/273/350/241/250_v3.4.md" +0 -0
- /package/docs/{ → mdcg/}/347/201/265/346/236/242_/350/207/252/346/210/221/345/261/202/345/256/232/344/271/211.md" +0 -0
- /package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276_MD/347/233/256/345/275/225/346/226/271/346/241/210_v0.1.md" +0 -0
- /package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276_/346/235/241/344/273/266/347/251/272/351/227/264/345/220/210/346/210/220/344/270/216/347/224/237/346/225/210/346/235/241/344/273/266/345/217/243/345/276/204_v0.1.md" +0 -0
- /package/docs/{ → mdcg/}/350/256/244/347/237/245/345/233/276/344/275/234/344/270/272/350/256/260/345/277/206/346/223/215/344/275/234/347/263/273/347/273/237_/350/257/204/344/274/260/344/270/216/350/267/257/347/272/277_v0.1.md" +0 -0
- /package/docs/{ → plans/}/345/221/275/345/220/215/346/262/273/347/220/206_/351/241/271/347/233/256/350/256/241/345/210/222.md" +0 -0
- /package/docs/{ → plans/}/347/237/245/350/257/206/345/233/276/350/260/261/351/241/271/347/233/256_/344/272/244/346/216/245/346/226/207/346/241/243_20260914.md" +0 -0
- /package/docs/{ → swarm/}/350/234/202/347/276/244/345/244/232/346/231/272/350/203/275/344/275/223_/344/275/277/347/224/250/346/212/245/345/221/212_20260913.md" +0 -0
- /package/docs/{ → swarm/}/350/234/202/347/276/244/345/244/232/346/231/272/350/203/275/344/275/223_/345/212/237/350/203/275/350/257/264/346/230/216_v0.6.md" +0 -0
- /package/docs/{ → swarm/}/350/234/202/347/276/244/347/233/262/345/214/272/346/240/207/350/256/260_v1.0.md" +0 -0
- /package/docs/{ → theory/}/344/277/241/346/201/257/345/267/256/344/270/272/344/273/200/344/271/210/345/277/205/347/204/266/345/255/230/345/234/250/344/270/224/350/207/252/347/204/266/346/211/251/345/244/247.md" +0 -0
- /package/docs/{ → theory/}/346/231/272/350/203/275/347/232/204/345/205/254/347/220/206/345/214/226/345/237/272/347/237/263.md" +0 -0
- /package/docs/{ → theory/}/346/231/272/350/203/275/347/232/204/350/256/244/347/237/245/350/277/207/347/250/213.md" +0 -0
- /package/docs/{ → theory/}/346/231/272/350/203/275/350/256/2723.4.md" +0 -0
- /package/docs/{ → theory/}/347/231/275/347/256/261/346/231/272/350/203/275/346/230/257/344/273/200/344/271/210/357/274/237.md" +0 -0
- /package/docs/{ → theory/}/347/231/275/347/256/261/346/231/272/350/203/275/347/263/273/345/210/227/302/267/347/254/254/344/272/224/347/257/207/357/274/232/350/256/251AI/347/234/237/346/255/243/350/243/205/344/270/212/350/256/260/345/277/206.md" +0 -0
package/md_cg/mdcos.py
CHANGED
|
@@ -23,17 +23,20 @@ import json
|
|
|
23
23
|
import os
|
|
24
24
|
import re
|
|
25
25
|
import time
|
|
26
|
+
import uuid
|
|
26
27
|
|
|
27
28
|
from .mdcg import (MdCG, expand_query_terms, bigrams, normalize_en, STATE_ACCEPT,
|
|
28
29
|
STATE_REJECT, STATE_DEFER, STATE_BLINDSPOT, TIER_BUCKET_LIKE,
|
|
29
30
|
TIER_BUCKET_SCAN, TIER_GLOBAL_LIKE, TIER_GLOBAL_SCAN,
|
|
30
31
|
GLOBAL_CAP, expand_query_terms_weighted,
|
|
31
|
-
expand_query_terms_llm, en_zh_bigrams, semantic_on
|
|
32
|
+
expand_query_terms_llm, en_zh_bigrams, semantic_on,
|
|
33
|
+
cut_by_relevance)
|
|
32
34
|
from . import (nodefile, routing, chain, subgraph, forgetting, protect,
|
|
33
35
|
identity, consistency, metacognition, crypto, sustain,
|
|
34
36
|
self_state, predict, evolution, weights, pooling,
|
|
35
37
|
writelimit)
|
|
36
|
-
from .fsutil import FileLock, atomic_write, append_jsonl, read_jsonl
|
|
38
|
+
from .fsutil import (FileLock, atomic_write, append_jsonl, read_jsonl,
|
|
39
|
+
count_jsonl)
|
|
37
40
|
from .security import (Principal, TenantRegistry, AccessDenied,
|
|
38
41
|
SENSITIVITY_ORDER, DEFAULT_SENSITIVITY, _rank)
|
|
39
42
|
|
|
@@ -275,6 +278,7 @@ class MdCGOS(MdCG):
|
|
|
275
278
|
super().__init__(root, autoflush=autoflush)
|
|
276
279
|
self.actor = actor
|
|
277
280
|
self.audit_log = os.path.join(self.root, "_audit.jsonl")
|
|
281
|
+
self.audit_archive = os.path.join(self.root, self.AUDIT_ARCHIVE)
|
|
278
282
|
self.deletions_log = os.path.join(self.root, "_deletions.jsonl")
|
|
279
283
|
self.hippocampus = os.path.join(self.root, self.HIPPOCAMPUS)
|
|
280
284
|
self.inbox_log = os.path.join(self.hippocampus, "inbox.jsonl")
|
|
@@ -282,6 +286,8 @@ class MdCGOS(MdCG):
|
|
|
282
286
|
self.trash_dir = os.path.join(self.root, self.TRASH)
|
|
283
287
|
os.makedirs(self.hippocampus, exist_ok=True)
|
|
284
288
|
os.makedirs(self.trash_dir, exist_ok=True)
|
|
289
|
+
self._audit_writes = 0 # 进程内写入计数(轮转探测节流,稳态零 stat)
|
|
290
|
+
self._audit_index = None # 归档索引缓存(懒加载)
|
|
285
291
|
|
|
286
292
|
# ================= 6. payload-free 审计 =================
|
|
287
293
|
|
|
@@ -291,12 +297,216 @@ class MdCGOS(MdCG):
|
|
|
291
297
|
"session": getattr(self, "session", None)}
|
|
292
298
|
rec.update(meta)
|
|
293
299
|
try:
|
|
300
|
+
self._rotate_audit_if_needed()
|
|
294
301
|
append_jsonl(self.audit_log, rec)
|
|
295
302
|
except OSError:
|
|
296
303
|
pass
|
|
297
304
|
|
|
298
|
-
def audit_records(self):
|
|
299
|
-
|
|
305
|
+
def audit_records(self, limit: int = None):
|
|
306
|
+
"""审计记录读取——轮转后跨分片按时间序(旧片在前)合并。
|
|
307
|
+
|
|
308
|
+
默认保持全量语义(既有调用方零改动);limit=N 取尾部 N 条,供巡检使用
|
|
309
|
+
——有界日志不该被读成新的 O(n) 全量。
|
|
310
|
+
"""
|
|
311
|
+
paths = [os.path.join(self.audit_archive, n) for n in self._audit_shards()]
|
|
312
|
+
paths.append(self.audit_log)
|
|
313
|
+
if limit is None:
|
|
314
|
+
out = []
|
|
315
|
+
for p in paths:
|
|
316
|
+
out.extend(read_jsonl(p))
|
|
317
|
+
return out
|
|
318
|
+
out = []
|
|
319
|
+
for p in reversed(paths): # 从最新往回读,读满 limit 即停
|
|
320
|
+
if len(out) >= limit:
|
|
321
|
+
break
|
|
322
|
+
out = list(read_jsonl(p)) + out
|
|
323
|
+
return out[-limit:]
|
|
324
|
+
|
|
325
|
+
# ---------- 审计日志分片轮转(治本:给无上界增长装上界) ----------
|
|
326
|
+
|
|
327
|
+
def _audit_shards(self):
|
|
328
|
+
"""归档分片名,序号零填充 ⇒ 字典序 == 时间序。"""
|
|
329
|
+
try:
|
|
330
|
+
names = os.listdir(self.audit_archive)
|
|
331
|
+
except OSError:
|
|
332
|
+
return []
|
|
333
|
+
return sorted(n for n in names
|
|
334
|
+
if n.startswith("_audit.") and n.endswith(".jsonl"))
|
|
335
|
+
|
|
336
|
+
def _load_audit_index(self) -> dict:
|
|
337
|
+
if self._audit_index is None:
|
|
338
|
+
idx = {}
|
|
339
|
+
try:
|
|
340
|
+
with open(os.path.join(self.audit_archive, self.AUDIT_INDEX),
|
|
341
|
+
"r", encoding="utf-8") as f:
|
|
342
|
+
data = json.load(f)
|
|
343
|
+
if isinstance(data, dict):
|
|
344
|
+
idx = {k: v for k, v in data.items() if isinstance(v, dict)}
|
|
345
|
+
except (OSError, ValueError):
|
|
346
|
+
idx = {} # 索引缺失/损坏 → 空起步,自愈补数
|
|
347
|
+
self._audit_index = idx
|
|
348
|
+
return self._audit_index
|
|
349
|
+
|
|
350
|
+
def _save_audit_index(self, idx: dict):
|
|
351
|
+
try:
|
|
352
|
+
os.makedirs(self.audit_archive, exist_ok=True)
|
|
353
|
+
atomic_write(os.path.join(self.audit_archive, self.AUDIT_INDEX),
|
|
354
|
+
json.dumps(idx, ensure_ascii=False, indent=1, sort_keys=True))
|
|
355
|
+
except OSError:
|
|
356
|
+
pass
|
|
357
|
+
self._audit_index = idx
|
|
358
|
+
|
|
359
|
+
def _rotate_audit_if_needed(self):
|
|
360
|
+
"""写前闸门:活动日志达阈值即切分(把 stat 摊到 1/AUDIT_PROBE_EVERY)。
|
|
361
|
+
|
|
362
|
+
为什么不逐条 stat:写路径要付的是「每次写入税」,而活动文件由本进程与同
|
|
363
|
+
root 其它进程共同增长,故按固定间隔读一次真实元数据校准——既不漏轮转,
|
|
364
|
+
也不把 O(1) 纪律反向变成 O(n) 写入开销。
|
|
365
|
+
"""
|
|
366
|
+
limit = self.AUDIT_ROTATE_BYTES
|
|
367
|
+
if limit <= 0:
|
|
368
|
+
return
|
|
369
|
+
self._audit_writes = (self._audit_writes or 0) + 1
|
|
370
|
+
if self._audit_writes % self.AUDIT_PROBE_EVERY:
|
|
371
|
+
return
|
|
372
|
+
try:
|
|
373
|
+
if os.path.getsize(self.audit_log) < limit:
|
|
374
|
+
return
|
|
375
|
+
except OSError:
|
|
376
|
+
return
|
|
377
|
+
self.rotate_audit()
|
|
378
|
+
|
|
379
|
+
def rotate_audit(self, reason: str = "size"):
|
|
380
|
+
"""把活动审计日志切分为归档分片(os.replace 原子,不重写一个字节)。
|
|
381
|
+
|
|
382
|
+
语义边界(诚实面):
|
|
383
|
+
· 分片内容与轮转前**逐行一致**(rename 不动字节);最新记录始终在活动文件
|
|
384
|
+
`_audit.jsonl` 中——读尾部取最新记录的调用方不受轮转影响;
|
|
385
|
+
· 并发由 FileLock + 「rename 前复检大小 / 失败即返回 None」兜住:抢输的
|
|
386
|
+
进程不重复切分,也不丢记录(记录要么在旧片、要么在活动文件);
|
|
387
|
+
· 保留策略只淘汰**分片**,且淘汰名单写进审计(不静默丢证据)。
|
|
388
|
+
"""
|
|
389
|
+
with FileLock(os.path.join(self.root, "_audit.rotate.lock"), timeout=5.0):
|
|
390
|
+
try:
|
|
391
|
+
size = os.path.getsize(self.audit_log)
|
|
392
|
+
except OSError:
|
|
393
|
+
return None
|
|
394
|
+
if size < self.AUDIT_ROTATE_BYTES:
|
|
395
|
+
return None # 已被并发写者轮转
|
|
396
|
+
os.makedirs(self.audit_archive, exist_ok=True)
|
|
397
|
+
name = "_audit.%06d.jsonl" % self._next_shard_seq()
|
|
398
|
+
dst = os.path.join(self.audit_archive, name)
|
|
399
|
+
try:
|
|
400
|
+
os.replace(self.audit_log, dst)
|
|
401
|
+
except OSError:
|
|
402
|
+
return None # 抢输(文件已被移走)→ 让位,不报错
|
|
403
|
+
scale = self._audit_count_shard(dst, size) # 分档:有界扫描 / 只读量级
|
|
404
|
+
events = scale["events"]
|
|
405
|
+
idx = self._load_audit_index()
|
|
406
|
+
idx[name] = {"bytes": size, "events": events, "exact": scale["exact"],
|
|
407
|
+
"t": round(time.time(), 3), "reason": reason}
|
|
408
|
+
self._save_audit_index(idx)
|
|
409
|
+
pruned = self._prune_audit_shards()
|
|
410
|
+
self._audit_writes = 0
|
|
411
|
+
try: # 自述留痕:轮转本身可审计
|
|
412
|
+
append_jsonl(self.audit_log,
|
|
413
|
+
{"t": time.time(), "op": "audit_rotate", "id": name,
|
|
414
|
+
"actor": self.actor, "bytes": size, "events": events,
|
|
415
|
+
"pruned": pruned, "reason": reason,
|
|
416
|
+
"session": getattr(self, "session", None)})
|
|
417
|
+
except OSError:
|
|
418
|
+
pass
|
|
419
|
+
return {"shard": name, "bytes": size, "events": events, "pruned": pruned}
|
|
420
|
+
|
|
421
|
+
def _audit_count_shard(self, path, size: int = None) -> dict:
|
|
422
|
+
"""分片条数读数:有界分片给精确值,超大历史分片只给量级。
|
|
423
|
+
|
|
424
|
+
为什么分档:阈值内的分片扫描是**有界**代价(≤ AUDIT_COUNT_MAX_BYTES);但
|
|
425
|
+
历史遗留的超大文件(本机首个分片即 4.0 GB)若在轮转/体检路径上全量解析,
|
|
426
|
+
就会把「一次调用堵死整条通道」原样复现——故超阈值退回 _log_scale 元数据
|
|
427
|
+
口径并如实标注 exact=False(要精确值走离线工具,不在写路径上付 O(n))。
|
|
428
|
+
"""
|
|
429
|
+
if size is None:
|
|
430
|
+
try:
|
|
431
|
+
size = os.path.getsize(path)
|
|
432
|
+
except OSError:
|
|
433
|
+
return {"bytes": 0, "events": 0, "exact": True}
|
|
434
|
+
if size <= self.AUDIT_COUNT_MAX_BYTES:
|
|
435
|
+
return {"bytes": size, "events": count_jsonl(path), "exact": True}
|
|
436
|
+
s = self._log_scale(path)
|
|
437
|
+
return {"bytes": size, "events": s["events"], "exact": False}
|
|
438
|
+
|
|
439
|
+
def _next_shard_seq(self) -> int:
|
|
440
|
+
top = 0
|
|
441
|
+
for n in self._audit_shards():
|
|
442
|
+
try:
|
|
443
|
+
top = max(top, int(n[len("_audit."):-len(".jsonl")]))
|
|
444
|
+
except ValueError:
|
|
445
|
+
continue
|
|
446
|
+
return top + 1
|
|
447
|
+
|
|
448
|
+
def _prune_audit_shards(self):
|
|
449
|
+
"""保留最近 AUDIT_KEEP_SHARDS 个分片、淘汰更旧的(≤0 表示不淘汰)。"""
|
|
450
|
+
keep = self.AUDIT_KEEP_SHARDS
|
|
451
|
+
if keep <= 0:
|
|
452
|
+
return []
|
|
453
|
+
shards = self._audit_shards()
|
|
454
|
+
gone = shards[:-keep] if len(shards) > keep else []
|
|
455
|
+
if not gone:
|
|
456
|
+
return []
|
|
457
|
+
idx = self._load_audit_index()
|
|
458
|
+
for n in gone:
|
|
459
|
+
try:
|
|
460
|
+
os.remove(os.path.join(self.audit_archive, n))
|
|
461
|
+
except OSError:
|
|
462
|
+
continue # 删不掉就留着:不假装已淘汰
|
|
463
|
+
idx.pop(n, None)
|
|
464
|
+
self._save_audit_index(idx)
|
|
465
|
+
return gone
|
|
466
|
+
|
|
467
|
+
def audit_scale(self) -> dict:
|
|
468
|
+
"""审计面量级读数(O(1) 稳态):活动文件实时读数 + 归档分片索引缓存。
|
|
469
|
+
|
|
470
|
+
为什么不逐片重数(第 4 条:指标与代价匹配):轮转后「总计」= 活动 + 各分片,
|
|
471
|
+
逐片全量数会把体检摊成 O(分片总字节)。分片是**封存文件**(内容不再变),故
|
|
472
|
+
封存时数一次、落索引缓存即得稳态 O(1);目录里出现未登记分片(外部手工放入)
|
|
473
|
+
时自愈补数一次——绝不假装与磁盘一致。
|
|
474
|
+
"""
|
|
475
|
+
active = self._log_scale(self.audit_log)
|
|
476
|
+
present = self._audit_shards()
|
|
477
|
+
idx = self._load_audit_index()
|
|
478
|
+
healed = False
|
|
479
|
+
for n in present:
|
|
480
|
+
if n in idx:
|
|
481
|
+
continue
|
|
482
|
+
p = os.path.join(self.audit_archive, n)
|
|
483
|
+
try:
|
|
484
|
+
sc = self._audit_count_shard(p) # 分档:有界扫描 / 只读量级
|
|
485
|
+
idx[n] = {"bytes": sc["bytes"], "events": sc["events"],
|
|
486
|
+
"exact": sc["exact"], "t": round(time.time(), 3),
|
|
487
|
+
"reason": "adopted"}
|
|
488
|
+
healed = True
|
|
489
|
+
except OSError:
|
|
490
|
+
continue
|
|
491
|
+
if healed:
|
|
492
|
+
self._save_audit_index(idx)
|
|
493
|
+
shard_bytes = sum(int(v.get("bytes") or 0) for k, v in idx.items() if k in present)
|
|
494
|
+
shard_events = sum(int(v.get("events") or 0) for k, v in idx.items() if k in present)
|
|
495
|
+
oversized = sum(1 for n in present if not idx.get(n, {}).get("exact", True))
|
|
496
|
+
out = dict(active)
|
|
497
|
+
out.update({
|
|
498
|
+
"shards": len(present),
|
|
499
|
+
"shard_bytes": shard_bytes,
|
|
500
|
+
"shard_events": shard_events,
|
|
501
|
+
"oversized": oversized, # 非精确分片数(历史遗留超大文件)
|
|
502
|
+
"total_bytes": active["bytes"] + shard_bytes,
|
|
503
|
+
"total_events": (active["events"] + shard_events
|
|
504
|
+
if active["events"] is not None else None),
|
|
505
|
+
"total_exact": bool(active["exact"]) and oversized == 0,
|
|
506
|
+
"rotate_bytes": self.AUDIT_ROTATE_BYTES,
|
|
507
|
+
"keep_shards": self.AUDIT_KEEP_SHARDS,
|
|
508
|
+
})
|
|
509
|
+
return out
|
|
300
510
|
|
|
301
511
|
# ================= 2. role 分层索引 =================
|
|
302
512
|
|
|
@@ -315,11 +525,14 @@ class MdCGOS(MdCG):
|
|
|
315
525
|
return nid
|
|
316
526
|
|
|
317
527
|
def _candidates(self, layer=None, roles=None, include_work=False,
|
|
318
|
-
session=None):
|
|
528
|
+
session=None, branch=None):
|
|
319
529
|
"""候选池:按层 + role 过滤。默认剔除工作角色(工具输出/命令/编辑)。
|
|
320
530
|
|
|
321
531
|
session:会话归属过滤(frontmatter.session,写入时自动落盘)——
|
|
322
532
|
多会话共用一个 root 时,按它区分「本会话记忆 / 其他会话记忆」。
|
|
533
|
+
branch:分支可见性(记忆演化分支④)——None(默认)时分支实验
|
|
534
|
+
节点全部隐身(实验不污染主支检索);branch=<id> 时主支 + 该分支
|
|
535
|
+
可见、其他分支仍隐身。
|
|
323
536
|
"""
|
|
324
537
|
out = []
|
|
325
538
|
for e in self.index["nodes"].values():
|
|
@@ -327,6 +540,8 @@ class MdCGOS(MdCG):
|
|
|
327
540
|
continue # 负记忆走覆盖标记;目标只做定向,都不进正排
|
|
328
541
|
if session and e.get("session") != session:
|
|
329
542
|
continue
|
|
543
|
+
if e.get("branch_id") not in (None, branch):
|
|
544
|
+
continue
|
|
330
545
|
if layer and e.get("layer") != layer:
|
|
331
546
|
continue
|
|
332
547
|
r = e.get("role")
|
|
@@ -376,7 +591,7 @@ class MdCGOS(MdCG):
|
|
|
376
591
|
context=None, min_results: int = 1, record: bool = True,
|
|
377
592
|
include_neg: bool = True, judge: bool = True,
|
|
378
593
|
roles=None, include_work: bool = False, pools=None,
|
|
379
|
-
session=None):
|
|
594
|
+
session=None, branch=None):
|
|
380
595
|
"""在父类语义之上加 role 过滤(默认剔除工具输出/命令/编辑)。
|
|
381
596
|
|
|
382
597
|
返回 (results, meta),与 MdCG.search 完全同构(T0–T3 阶梯 + 资格判定)。
|
|
@@ -387,7 +602,7 @@ class MdCGOS(MdCG):
|
|
|
387
602
|
return [], {"tier": None, "reason": "empty_query", "scanned": 0}
|
|
388
603
|
pool_cfg = pooling.resolve(pooling.from_env(pools))
|
|
389
604
|
entries = self._candidates(layer=layer, roles=roles, include_work=include_work,
|
|
390
|
-
session=session)
|
|
605
|
+
session=session, branch=branch)
|
|
391
606
|
if not entries:
|
|
392
607
|
return [], {"tier": None, "reason": "no_candidates", "scanned": 0}
|
|
393
608
|
|
|
@@ -428,25 +643,27 @@ class MdCGOS(MdCG):
|
|
|
428
643
|
if out:
|
|
429
644
|
return out
|
|
430
645
|
|
|
646
|
+
# 截断依据=相关度(同 MdCG.search:cap 值不变,改的是拿什么排序)
|
|
431
647
|
docs_all = self._read_many(entries, stat)
|
|
432
648
|
# 语义资格(MDCG_SEMANTIC=1):fm.semantic 节点无条件入池
|
|
433
649
|
hits = [d for d in docs_all if self._like(d[2], d[1], terms)
|
|
434
650
|
or (semantic_on() and d[1].get("semantic"))]
|
|
435
651
|
stat["pre_cap"] = len(hits)
|
|
436
652
|
stat["cap"] = GLOBAL_CAP
|
|
437
|
-
hits, _rep =
|
|
438
|
-
|
|
653
|
+
hits, _rep = cut_by_relevance(hits, self._score(hits, q, qb, pool_cfg),
|
|
654
|
+
GLOBAL_CAP, pools=pool_cfg,
|
|
655
|
+
key_of=pooling.doc_key, stat=stat)
|
|
439
656
|
pooling.record_audit(stat, _rep)
|
|
440
657
|
out = try_stage(hits, TIER_GLOBAL_LIKE)
|
|
441
658
|
if out:
|
|
442
659
|
return out
|
|
443
660
|
|
|
444
|
-
docs_all.sort(key=lambda d: (-float(d[1].get("importance") or 0),
|
|
445
|
-
-float(d[1].get("created_at") or 0)))
|
|
446
661
|
stat["pre_cap"] = len(docs_all)
|
|
447
662
|
stat["cap"] = GLOBAL_CAP
|
|
448
|
-
picked, _rep =
|
|
449
|
-
|
|
663
|
+
picked, _rep = cut_by_relevance(docs_all,
|
|
664
|
+
self._score(docs_all, q, qb, pool_cfg),
|
|
665
|
+
GLOBAL_CAP, pools=pool_cfg,
|
|
666
|
+
key_of=pooling.doc_key, stat=stat)
|
|
450
667
|
pooling.record_audit(stat, _rep)
|
|
451
668
|
scored = self._score(picked, q, qb, pool_cfg)
|
|
452
669
|
return self._emit(scored, k, TIER_GLOBAL_SCAN, stat, route_bucket,
|
|
@@ -460,7 +677,9 @@ class MdCGOS(MdCG):
|
|
|
460
677
|
LIKE 命中集沿 entries(目录枚举序)排列,插入序截断会让本路候选池
|
|
461
678
|
随写入顺序漂移、不可复算,并可能把与查询最相关的节点随机丢弃。
|
|
462
679
|
LIKE 全空时兜底池改用 importance/created_at 序(与 search 主路径同口径),
|
|
463
|
-
不再取插入序前 CAP
|
|
680
|
+
不再取插入序前 CAP。返回序**恒为相关度降序**——原实现仅在超 cap 时
|
|
681
|
+
排序,≤cap 时直接返回 entries 枚举序,下游 seed/融合会拿到无语义依据
|
|
682
|
+
的顺序。
|
|
464
683
|
"""
|
|
465
684
|
terms = expand_query_terms(query)
|
|
466
685
|
# qb 与文档侧 normalize_en 口径对齐(同 MdCGOS.search,防大小写断裂)
|
|
@@ -472,16 +691,23 @@ class MdCGOS(MdCG):
|
|
|
472
691
|
hits = [d for d in docs if self._like(d[2], d[1], terms)
|
|
473
692
|
or (semantic_on() and d[1].get("semantic"))]
|
|
474
693
|
if not hits:
|
|
694
|
+
# 兜底池(LIKE 全空 = 无相关度信号):截断依据=importance/created_at
|
|
695
|
+
# 序,确定可复算;此时 bigram 部分匹配不足以定序(共现噪声),
|
|
696
|
+
# 故本路不做「先全量打分再截断」。
|
|
475
697
|
hits = sorted(
|
|
476
698
|
docs, key=lambda d: (-float(d[1].get("importance") or 0),
|
|
477
699
|
-float(d[1].get("created_at") or 0))
|
|
478
700
|
)[:GLOBAL_CAP]
|
|
701
|
+
stat["cut_order"] = "importance_fallback"
|
|
479
702
|
scored = self._score(hits, query, qb)
|
|
703
|
+
# 恒按相关度排序(原实现仅超 cap 时排序 → ≤cap 时返回 entries 枚举序,
|
|
704
|
+
# 下游 seed/融合拿到无语义依据的顺序,不可复算)
|
|
705
|
+
scored.sort(key=lambda x: (-x[1],
|
|
706
|
+
-float(x[0]["frontmatter"].get("importance") or 0)))
|
|
480
707
|
if len(scored) > GLOBAL_CAP:
|
|
481
708
|
stat["pre_cap"] = len(scored)
|
|
482
709
|
stat["cap"] = GLOBAL_CAP
|
|
483
|
-
|
|
484
|
-
-float(x[0]["frontmatter"].get("importance") or 0)))
|
|
710
|
+
stat["cut_order"] = "relevance"
|
|
485
711
|
return scored[:GLOBAL_CAP]
|
|
486
712
|
return scored
|
|
487
713
|
|
|
@@ -728,7 +954,7 @@ class MdCGOS(MdCG):
|
|
|
728
954
|
record: bool = True, query_expand=None,
|
|
729
955
|
path_weights=None, recall_only=None, fusion: str = "sum",
|
|
730
956
|
goal_text=None, judge_ranking: bool = False,
|
|
731
|
-
session=None):
|
|
957
|
+
session=None, branch=None):
|
|
732
958
|
"""并行多路召回 + RRF 融合。返回 (results, meta)。
|
|
733
959
|
|
|
734
960
|
每路各自排序 → Reciprocal Rank Fusion:
|
|
@@ -763,7 +989,7 @@ class MdCGOS(MdCG):
|
|
|
763
989
|
if not q:
|
|
764
990
|
return [], {"tier": None, "reason": "empty_query", "paths": {}}
|
|
765
991
|
entries = self._candidates(layer=layer, roles=roles, include_work=include_work,
|
|
766
|
-
session=session)
|
|
992
|
+
session=session, branch=branch)
|
|
767
993
|
if not entries:
|
|
768
994
|
return [], {"tier": None, "reason": "no_candidates", "paths": {}}
|
|
769
995
|
|
|
@@ -873,7 +1099,7 @@ class MdCGOS(MdCG):
|
|
|
873
1099
|
include_work: bool = False, judge: bool = True, use_rrf: bool = True,
|
|
874
1100
|
paths=None, query_expand=None, fusion=None,
|
|
875
1101
|
goal_text=None, include_recent=False, recent_limit: int = 10,
|
|
876
|
-
judge_ranking: bool = False, session=None,
|
|
1102
|
+
judge_ranking: bool = False, session=None, branch=None,
|
|
877
1103
|
max_item_tokens: int = DEFAULT_MAX_ITEM_TOKENS):
|
|
878
1104
|
"""按 token 预算装包:装到预算花完为止。
|
|
879
1105
|
|
|
@@ -906,11 +1132,13 @@ class MdCGOS(MdCG):
|
|
|
906
1132
|
if goal_text is not None:
|
|
907
1133
|
kw["goal_text"] = goal_text
|
|
908
1134
|
kw["judge_ranking"] = judge_ranking
|
|
1135
|
+
if branch is not None:
|
|
1136
|
+
kw["branch"] = branch
|
|
909
1137
|
results, meta = self.search_rrf(query, **kw)
|
|
910
1138
|
items = [(r[0], r[1], r[2], r[3]) for r in results]
|
|
911
1139
|
else:
|
|
912
1140
|
res, meta = self.search(query, layer=layer, k=k, context=context,
|
|
913
|
-
judge=judge, session=session)
|
|
1141
|
+
judge=judge, session=session, branch=branch)
|
|
914
1142
|
items = [(r[0], r[1], r[2], []) for r in res]
|
|
915
1143
|
|
|
916
1144
|
pack, skipped, used = [], [], 0
|
|
@@ -1033,24 +1261,67 @@ class MdCGOS(MdCG):
|
|
|
1033
1261
|
# ================= 4. 审核队列(inbox → decisions) =================
|
|
1034
1262
|
|
|
1035
1263
|
def propose(self, node_id: str, content: str, layer: str = "knowledge",
|
|
1036
|
-
tags=None, condition_space=None, verify=None,
|
|
1264
|
+
tags=None, condition_space=None, verify=None,
|
|
1265
|
+
info: bool = False, **kw):
|
|
1037
1266
|
"""把一个候选记忆放入海马体 inbox,等待审核(不直接持久化)。
|
|
1038
1267
|
|
|
1039
1268
|
verify —— 验收判据(内联声明,裁决阶段只读),形如:
|
|
1040
1269
|
{"kind": "code", "assertions": ["pytest -k foo 通过"], "cmd": "..."}
|
|
1041
1270
|
判据指纹随提案落盘,复核者只能按原判据裁决,不能放宽标准。
|
|
1271
|
+
|
|
1272
|
+
幂等对账(两段式):入队动作在 strict 锁内「查重 → 入队」原子完成。
|
|
1273
|
+
对账键 = payload_hash(内容签名 _sig(content)),同内容提案若已存在
|
|
1274
|
+
(无论 pending 还是已裁决 accepted/rejected)→ 幂等返回既有 pid,
|
|
1275
|
+
不再入队。语义:重试与崩溃恢复无害——入队落盘成功而响应丢失时
|
|
1276
|
+
(MCP 客户端超时重试的真正机制),重试对账命中既有记录并返回原 pid。
|
|
1277
|
+
|
|
1278
|
+
返回值:info=False(默认)返回 pid 字符串(保形,存量调用零变化);
|
|
1279
|
+
info=True 返回 {"pid", "dedup", "dup_of", "dup_status"}。
|
|
1042
1280
|
"""
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1281
|
+
phash = _sig(content)
|
|
1282
|
+
with FileLock(self.inbox_log, strict=True):
|
|
1283
|
+
st = self._pid_status()
|
|
1284
|
+
dup = None
|
|
1285
|
+
for r in read_jsonl(self.inbox_log):
|
|
1286
|
+
# 存量记录无 payload_hash 字段 → 现算兼容(对账覆盖旧账)
|
|
1287
|
+
rh = r.get("payload_hash") or _sig(r.get("content") or "")
|
|
1288
|
+
if rh != phash:
|
|
1289
|
+
continue
|
|
1290
|
+
s = st.get(r.get("pid")) or {}
|
|
1291
|
+
dup = {"pid": r.get("pid"),
|
|
1292
|
+
"status": s.get("status") or "pending",
|
|
1293
|
+
"node_id": r.get("id")}
|
|
1294
|
+
if dup["status"] in ("accepted", "rejected"):
|
|
1295
|
+
break # 已裁决的最有信息量,优先返回
|
|
1296
|
+
if dup:
|
|
1297
|
+
self._audit("propose_dedup", node_id, dup_of=dup["pid"],
|
|
1298
|
+
dup_status=dup["status"], payload_hash=phash)
|
|
1299
|
+
if info:
|
|
1300
|
+
return {"pid": dup["pid"], "dedup": True,
|
|
1301
|
+
"dup_of": dup["pid"], "dup_status": dup["status"]}
|
|
1302
|
+
return dup["pid"]
|
|
1303
|
+
# 唯一性取证(2026-09-16):旧式 _sig(node_id + time.time()) 只带
|
|
1304
|
+
# 「节点 id + 时间戳」,无进程熵——多 worker 在同一时刻用相同
|
|
1305
|
+
# node_id 入队即产生 pid 碰撞(test_review_conformance【8】「pid
|
|
1306
|
+
# 互不重复」偶发红,4 进程同用 w-node-K 时命中)。幂等由
|
|
1307
|
+
# payload_hash 对账保证、与 pid 取值无关,故此处补熵是
|
|
1308
|
+
# 只增不减契约(20 条互异由 test_review_conformance【8c】守卫)。
|
|
1309
|
+
pid = "prop_" + _sig(node_id + str(time.time())
|
|
1310
|
+
+ uuid.uuid4().hex)
|
|
1311
|
+
_norm, vhash = _verify_norm(verify)
|
|
1312
|
+
rec = {"t": time.time(), "pid": pid, "id": node_id, "content": content,
|
|
1313
|
+
"layer": layer, "tags": list(tags or []),
|
|
1314
|
+
"condition_space": condition_space or {},
|
|
1315
|
+
"payload_hash": phash,
|
|
1316
|
+
"verify": verify or {}, "verify_hash": vhash,
|
|
1317
|
+
"extra": kw, "actor": self.actor,
|
|
1318
|
+
"session": getattr(self, "session", None)}
|
|
1319
|
+
append_jsonl(self.inbox_log, rec)
|
|
1052
1320
|
self._audit("propose", node_id, pid=pid, layer=layer,
|
|
1053
|
-
payload_hash=
|
|
1321
|
+
payload_hash=phash, verify_hash=vhash)
|
|
1322
|
+
if info:
|
|
1323
|
+
return {"pid": pid, "dedup": False,
|
|
1324
|
+
"dup_of": None, "dup_status": None}
|
|
1054
1325
|
return pid
|
|
1055
1326
|
|
|
1056
1327
|
def _pid_status(self):
|
|
@@ -1160,7 +1431,10 @@ class MdCGOS(MdCG):
|
|
|
1160
1431
|
rec["record_hash"] = self._record_hash(rec)
|
|
1161
1432
|
rec["result"] = {k: v for k, v in result.items()
|
|
1162
1433
|
if k in ("ok", "node_id", "error", "state")}
|
|
1163
|
-
|
|
1434
|
+
# 裁决记录不能丢:strict 锁内追加(并发裁决不交错;
|
|
1435
|
+
# 丢一条裁决会让提案回 pending → 重复落盘,比让裁决者等一下代价大)
|
|
1436
|
+
with FileLock(self.decisions_log, strict=True):
|
|
1437
|
+
append_jsonl(self.decisions_log, rec)
|
|
1164
1438
|
# md 审计节点写失败不影响裁决(jsonl 仍是权威来源)
|
|
1165
1439
|
try:
|
|
1166
1440
|
self._write_review_record(rec)
|
|
@@ -1176,6 +1450,43 @@ class MdCGOS(MdCG):
|
|
|
1176
1450
|
record_node=rec["record_node_id"])
|
|
1177
1451
|
return result
|
|
1178
1452
|
|
|
1453
|
+
def _cascade_dedup(self, pid: str, phash: str, decision: str):
|
|
1454
|
+
"""主提案终态裁决后,同 payload_hash 的其余纯 pending 提案自动出清。
|
|
1455
|
+
|
|
1456
|
+
兄弟提案多为超时重试的重复入队产物(存量账本里已有实例):主提案已
|
|
1457
|
+
裁决后,兄弟再走一遍裁决只会重复落盘/重复入库。只在 decisions 锁内
|
|
1458
|
+
「查兄弟最新状态 → 未有任何裁决记录才出清」,与并发裁决互斥,不会
|
|
1459
|
+
覆盖兄弟自己的 accept。needs_reapproval 的兄弟有独立红队轮次历史,
|
|
1460
|
+
不动(留人工处置)。级联裁决的权威源同为本文件(jsonl),
|
|
1461
|
+
不写 md 审计节点(可能批量,self 层只留 jsonl + audit 簿记)。
|
|
1462
|
+
"""
|
|
1463
|
+
if not phash:
|
|
1464
|
+
return []
|
|
1465
|
+
closed = []
|
|
1466
|
+
with FileLock(self.decisions_log, strict=True):
|
|
1467
|
+
st = self._pid_status()
|
|
1468
|
+
for r in read_jsonl(self.inbox_log):
|
|
1469
|
+
bpid = r.get("pid")
|
|
1470
|
+
if not bpid or bpid == pid or bpid in st:
|
|
1471
|
+
continue # 自己 / 已有裁决记录(含 needs_reapproval)跳过
|
|
1472
|
+
rh = r.get("payload_hash") or _sig(r.get("content") or "")
|
|
1473
|
+
if rh != phash:
|
|
1474
|
+
continue
|
|
1475
|
+
rec = {"t": time.time(), "pid": bpid, "decision": "reject",
|
|
1476
|
+
"status": "rejected", "round": 1,
|
|
1477
|
+
"redteam_verdict": "absent", "issues": [],
|
|
1478
|
+
"verify_hash": r.get("verify_hash") or "",
|
|
1479
|
+
"reason": f"cascade_dedup: 同内容提案已由 {pid} "
|
|
1480
|
+
f"{decision}(自动出清重复入队,无需再裁决)",
|
|
1481
|
+
"actor": self.actor, "target_id": r.get("id"),
|
|
1482
|
+
"record_node_id": self._audit_node_id(bpid, 1)}
|
|
1483
|
+
rec["record_hash"] = self._record_hash(rec)
|
|
1484
|
+
append_jsonl(self.decisions_log, rec)
|
|
1485
|
+
closed.append(bpid)
|
|
1486
|
+
for b in closed:
|
|
1487
|
+
self._audit("review_cascade", b, cascade_of=pid, decision=decision)
|
|
1488
|
+
return closed
|
|
1489
|
+
|
|
1179
1490
|
def review_records(self, pid: str = None):
|
|
1180
1491
|
"""列出裁决记录节点(self 层 / audit 标签),供外部来源审计。"""
|
|
1181
1492
|
out = []
|
|
@@ -1299,6 +1610,12 @@ class MdCGOS(MdCG):
|
|
|
1299
1610
|
extra["verify_hash"] = expect
|
|
1300
1611
|
nid = self.add(item["id"], content, layer=layer, tags=tags,
|
|
1301
1612
|
condition_space=item.get("condition_space"), **extra)
|
|
1613
|
+
# 索引增量收尾(2026-09-16 取证):add 只把条目放进本进程内存 _dirty,
|
|
1614
|
+
# 未达 autoflush(64) 阈值时进程退出即永久丢失——裁决进程(review_cli /
|
|
1615
|
+
# MCP op=review)通常只写 1~2 条,不 flush 则「节点在盘上但索引无条目」,
|
|
1616
|
+
# 其他进程与重载后的长驻进程都检索不到,只能靠某次全量 rebuild 偶然救回。
|
|
1617
|
+
# merge 分支绕开 add 直写节点文件,故其收尾同为索引重建(同因不同法)。
|
|
1618
|
+
self.flush()
|
|
1302
1619
|
result.update(ok=True, node_id=nid)
|
|
1303
1620
|
else: # merge
|
|
1304
1621
|
target = merge_into or (item.get("extra") or {}).get("merge_into")
|
|
@@ -1319,10 +1636,23 @@ class MdCGOS(MdCG):
|
|
|
1319
1636
|
self.rebuild_index()
|
|
1320
1637
|
result.update(ok=True, node_id=target)
|
|
1321
1638
|
|
|
1322
|
-
|
|
1639
|
+
result = self._record_decision(
|
|
1323
1640
|
pid, item, decision,
|
|
1324
1641
|
"rejected" if decision == "reject" else "accepted",
|
|
1325
1642
|
reason, round_no, rt_v, rt_issues, expect, result)
|
|
1643
|
+
# 级联出清:主提案已终态,同内容兄弟提案(重复入队产物)自动关闭
|
|
1644
|
+
casc = self._cascade_dedup(
|
|
1645
|
+
pid, item.get("payload_hash") or _sig(item.get("content") or ""),
|
|
1646
|
+
decision)
|
|
1647
|
+
if casc:
|
|
1648
|
+
result["cascade_closed"] = casc
|
|
1649
|
+
# 裁决链路统一收尾(2026-09-16 取证):上面 accept/edit 分支的 flush 只覆盖
|
|
1650
|
+
# 目标节点,而 `_record_decision → _write_review_record → add` 在这里又写了一个
|
|
1651
|
+
# **审计记录节点**(self 层),reject 路径更是**只**写它——两者同样停留在内存
|
|
1652
|
+
# `_dirty` 中。不在此收尾则「审计记录在盘上但索引无条目」,复核方与其它进程都
|
|
1653
|
+
# 检索不到(与目标节点同因,只是漏点不同)。flush 幂等(`_dirty` 空即返回)。
|
|
1654
|
+
self.flush()
|
|
1655
|
+
return result
|
|
1326
1656
|
|
|
1327
1657
|
def decisions(self):
|
|
1328
1658
|
return list(read_jsonl(self.decisions_log))
|
|
@@ -1352,8 +1682,9 @@ class MdCGOS(MdCG):
|
|
|
1352
1682
|
"actor": self.actor,
|
|
1353
1683
|
"trash": os.path.relpath(dst, self.root)
|
|
1354
1684
|
.replace("\\", "/")})
|
|
1355
|
-
|
|
1356
|
-
|
|
1685
|
+
# 摘索引必须**落盘**(写删除记录):只 pop 内存会让条目在下次启动
|
|
1686
|
+
# 重放 _index_log 时复活成幽灵条目(索引有条目、文件已进 trash/)。
|
|
1687
|
+
self._unstage(node_id)
|
|
1357
1688
|
subgraph.invalidate_cache(self)
|
|
1358
1689
|
chain.invalidate_cache(self)
|
|
1359
1690
|
self._audit("forget", node_id, reason=reason, payload_hash=h)
|
|
@@ -1480,14 +1811,21 @@ class MdCGOS(MdCG):
|
|
|
1480
1811
|
budget_tokens=1200, include_state=True):
|
|
1481
1812
|
"""按需恢复:一次调用返回「可续接的上下文包」(替代 hook 自动注入)。
|
|
1482
1813
|
|
|
1483
|
-
内容 = 最近会话要点 + 活跃目标 +
|
|
1484
|
-
|
|
1485
|
-
|
|
1814
|
+
内容 = 最近会话要点 + 活跃目标 + **任务台账(进行中 + 近期完成)** + 近期事件
|
|
1815
|
+
+ 未解问题 (+ 自我状态卡)。纯只读、无副作用;返回体受 budget_tokens 约束
|
|
1816
|
+
(超出即裁剪并显式上报)。无 hook 的载体应在会话开始时显式调用本 op 一次。
|
|
1817
|
+
|
|
1818
|
+
任务段(2026-09-16 新增)是「忘记已实现的工程」的直接解药:新会话开机即见
|
|
1819
|
+
「还在做的」与「刚做完的」,不必先想到去查。任务属结构层、跨会话稳定,
|
|
1820
|
+
故**不按 session 过滤**——工程台账跟着工程走,不跟着会话走。
|
|
1486
1821
|
"""
|
|
1487
1822
|
limit = max(1, min(int(limit or 5), 50))
|
|
1488
1823
|
budget = max(200, int(budget_tokens or 1200))
|
|
1489
1824
|
pack = {"ok": True, "session": session, "source": "session_recall",
|
|
1490
|
-
"notes": [], "goals": [],
|
|
1825
|
+
"notes": [], "goals": [],
|
|
1826
|
+
"tasks": {"active": [], "done": [], "active_total": 0,
|
|
1827
|
+
"done_total": 0},
|
|
1828
|
+
"recent": [], "unresolved": [],
|
|
1491
1829
|
"degraded": []}
|
|
1492
1830
|
# ① 会话要点
|
|
1493
1831
|
try:
|
|
@@ -1501,6 +1839,22 @@ class MdCGOS(MdCG):
|
|
|
1501
1839
|
for g in self.active_goals(limit=5)]
|
|
1502
1840
|
except Exception: # noqa: BLE001
|
|
1503
1841
|
pack["degraded"].append("goals")
|
|
1842
|
+
# ②.5 任务台账(structural 层)——见 docstring:不按 session 过滤
|
|
1843
|
+
try:
|
|
1844
|
+
from . import tasks as _tasks
|
|
1845
|
+
ts = _tasks.session_tasks(self, active_limit=5, done_limit=5)
|
|
1846
|
+
pack["tasks"] = {
|
|
1847
|
+
"active": [{"id": t["id"], "name": t["name"], "status": t["status"],
|
|
1848
|
+
"plan": (t.get("plan") or "")[:300],
|
|
1849
|
+
"updated_at": t.get("updated_at")}
|
|
1850
|
+
for t in ts["active"]],
|
|
1851
|
+
"done": [{"id": t["id"], "name": t["name"], "status": t["status"],
|
|
1852
|
+
"result": (t.get("result") or "")[:300],
|
|
1853
|
+
"updated_at": t.get("updated_at")}
|
|
1854
|
+
for t in ts["done"]],
|
|
1855
|
+
"active_total": ts["active_total"], "done_total": ts["done_total"]}
|
|
1856
|
+
except Exception: # noqa: BLE001
|
|
1857
|
+
pack["degraded"].append("tasks")
|
|
1504
1858
|
# ③ 近期事件(原始滚动窗口)
|
|
1505
1859
|
try:
|
|
1506
1860
|
evs = self.recent_events(limit=max(1, int(recent_limit or 10)))
|
|
@@ -1528,18 +1882,29 @@ class MdCGOS(MdCG):
|
|
|
1528
1882
|
pack["self_state"] = _ss.summary(self, session=session)
|
|
1529
1883
|
except Exception: # noqa: BLE001
|
|
1530
1884
|
pack["degraded"].append("self_state")
|
|
1531
|
-
# ⑥ 预算裁剪:交替丢 recent / notes
|
|
1885
|
+
# ⑥ 预算裁剪:交替丢 recent / notes 尾部;任务段**最后才让位**
|
|
1886
|
+
# (任务台账是结构性结论,事件流水是易失过程——先丢过程),
|
|
1887
|
+
# 被裁的事实在 tasks_truncated 里显式上报,不静默丢。
|
|
1888
|
+
tasks_trimmed = False
|
|
1532
1889
|
pack["tokens"] = est_tokens(json.dumps(pack, ensure_ascii=False))
|
|
1533
|
-
while pack["tokens"] > budget and (pack["recent"] or pack["notes"]
|
|
1534
|
-
|
|
1890
|
+
while pack["tokens"] > budget and (pack["recent"] or pack["notes"]
|
|
1891
|
+
or pack["tasks"]["active"]
|
|
1892
|
+
or pack["tasks"]["done"]):
|
|
1893
|
+
if pack["recent"] and len(pack["recent"]) >= len(pack["notes"]):
|
|
1535
1894
|
pack["recent"].pop()
|
|
1536
|
-
|
|
1895
|
+
elif pack["notes"]:
|
|
1537
1896
|
pack["notes"].pop()
|
|
1897
|
+
elif pack["tasks"]["done"] or pack["tasks"]["active"]:
|
|
1898
|
+
(pack["tasks"]["done"] or pack["tasks"]["active"]).pop()
|
|
1899
|
+
tasks_trimmed = True
|
|
1900
|
+
else:
|
|
1901
|
+
pack["recent"].pop()
|
|
1538
1902
|
pack["tokens"] = est_tokens(json.dumps(pack, ensure_ascii=False))
|
|
1539
1903
|
pack["budget_tokens"] = budget
|
|
1540
1904
|
pack["truncated"] = pack["tokens"] > budget
|
|
1541
|
-
pack["
|
|
1542
|
-
|
|
1905
|
+
pack["tasks_truncated"] = tasks_trimmed
|
|
1906
|
+
pack["note"] = ("只读上下文包:会话要点 + 目标 + 任务台账(进行中/近期完成)"
|
|
1907
|
+
"+ 近期事件 + 未解问题(+自我状态卡)。库侧替代 hook 自动注入;"
|
|
1543
1908
|
"会话开始时显式调用本 op 一次即可续接。")
|
|
1544
1909
|
return pack
|
|
1545
1910
|
|
|
@@ -1859,7 +2224,9 @@ class MdCGOS(MdCG):
|
|
|
1859
2224
|
# 三者都不进默认召回热路径,只在显式调用时工作。
|
|
1860
2225
|
|
|
1861
2226
|
INSIGHT_ACTIONS = ("window", "record", "verify", "list", "report",
|
|
1862
|
-
"reconstruct", "learn", "outlook", "catalog"
|
|
2227
|
+
"reconstruct", "learn", "outlook", "catalog",
|
|
2228
|
+
"fork", "branch_rewrite", "branch_search",
|
|
2229
|
+
"branch_merge", "branch_discard", "branches")
|
|
1863
2230
|
|
|
1864
2231
|
def insight(self, action="outlook", **kw):
|
|
1865
2232
|
"""洞察条件层 + 情景重构 + 盲区学习 + 结构洞察(P2)。
|
|
@@ -1909,6 +2276,34 @@ class MdCGOS(MdCG):
|
|
|
1909
2276
|
max_nodes=(kw.get("max_nodes") or subgraph.RECON_MAX_NODES),
|
|
1910
2277
|
neighbors=(True if kw.get("neighbors") is None
|
|
1911
2278
|
else bool(kw.get("neighbors"))))
|
|
2279
|
+
if act in ("fork", "branch_rewrite", "branch_search", "branch_merge",
|
|
2280
|
+
"branch_discard", "branches"):
|
|
2281
|
+
# 记忆演化分支(Pi 移植④):fork / 分支改写(唯一正路,归属强制
|
|
2282
|
+
# 继承)/ 分支内检索 / 溯源合并 / 教训归档冷收 / 盘点。
|
|
2283
|
+
# discard 属写操作(MCP 分发层 require_admin)。
|
|
2284
|
+
from . import branches as _br
|
|
2285
|
+
if act == "fork":
|
|
2286
|
+
return _br.fork(self, kw.get("node_ids") or kw.get("ids"),
|
|
2287
|
+
branch_id=kw.get("branch_id"),
|
|
2288
|
+
note=kw.get("note"))
|
|
2289
|
+
if act == "branch_rewrite":
|
|
2290
|
+
return _br.rewrite(self,
|
|
2291
|
+
kw.get("node_id") or kw.get("pid") or "",
|
|
2292
|
+
kw.get("content") or "",
|
|
2293
|
+
tags=kw.get("tags"),
|
|
2294
|
+
importance=kw.get("importance"))
|
|
2295
|
+
if act == "branch_search":
|
|
2296
|
+
return _br.search(self,
|
|
2297
|
+
kw.get("content") or kw.get("query") or "",
|
|
2298
|
+
kw.get("branch_id") or "")
|
|
2299
|
+
if act == "branch_merge":
|
|
2300
|
+
return _br.merge(self, kw.get("branch_id"),
|
|
2301
|
+
reason=kw.get("reason"))
|
|
2302
|
+
if act == "branch_discard":
|
|
2303
|
+
return _br.discard(self, kw.get("branch_id"),
|
|
2304
|
+
summary=kw.get("content")
|
|
2305
|
+
or kw.get("summary") or "")
|
|
2306
|
+
return _br.list_branches(self)
|
|
1912
2307
|
if act == "explore":
|
|
1913
2308
|
# 信息差驱动自主探索(opt-in):提案 → 五态验证 → 回写 gap_hint
|
|
1914
2309
|
from . import autonomy
|
|
@@ -1947,14 +2342,81 @@ class MdCGOS(MdCG):
|
|
|
1947
2342
|
|
|
1948
2343
|
# ================= 健康度(并入 OS 指标) =================
|
|
1949
2344
|
|
|
2345
|
+
AUDIT_COUNT_MAX_BYTES = 64 << 20 # 超过此规模不再全量精确计数(O(1) 体检纪律)
|
|
2346
|
+
# ---- 审计日志分片轮转(治本面,2026-09-16)----
|
|
2347
|
+
# 元数据读数(_log_scale)只让「指标与代价错配」不再显形,**有界性的来源是轮转**:
|
|
2348
|
+
# 单文件 ≤ AUDIT_ROTATE_BYTES、分片数 ≤ AUDIT_KEEP_SHARDS ⇒ 单片读取代价与总量
|
|
2349
|
+
# 都有上界,体检/getsize 不再随运行时长线性劣化。
|
|
2350
|
+
AUDIT_ROTATE_BYTES = 64 << 20 # 活动日志轮转阈值(≤0 关闭轮转,退回无上界)
|
|
2351
|
+
AUDIT_KEEP_SHARDS = 8 # 归档分片保留数(≤0 不淘汰;淘汰必留审计痕)
|
|
2352
|
+
AUDIT_ARCHIVE = "_audit_archive" # 分片归档目录(不在 LAYERS 内,不参与节点索引)
|
|
2353
|
+
AUDIT_INDEX = "_index.json" # 归档索引:分片 bytes/events 缓存(稳态 O(1))
|
|
2354
|
+
AUDIT_PROBE_EVERY = 32 # 每 N 次写入探测一次大小(把写入税摊到 1/N)
|
|
2355
|
+
|
|
2356
|
+
def _log_scale(self, path, est_sample=256 << 10):
|
|
2357
|
+
"""日志量级读数(O(1)):以元数据为主,条数只在与规模相称时才精确。
|
|
2358
|
+
|
|
2359
|
+
为什么不去全量数条数(2026-09-16 使用者裁定「python 审查 4GB 数据是个
|
|
2360
|
+
不明智的选择」+ 第 4 条现场取证):
|
|
2361
|
+
|
|
2362
|
+
信息需求 = 量级(这条日志多大、多久没动)→ O(1) 元数据即可满足;
|
|
2363
|
+
实际代价 = 全量解析/扫描 → O(n) 磁盘 IO(本机 4.0 GB 实测数十秒);
|
|
2364
|
+
且日志**无上界增长**(实测 ~2 条/s 持续写入、累计 ~21.5 M 条 / 4.0 GB)。
|
|
2365
|
+
指标与代价错配,而「无上界」使任何「每次体检全量扫」的实现随运行时长
|
|
2366
|
+
线性劣化——4 GB 只是让错配显形,不是错配本身。
|
|
2367
|
+
|
|
2368
|
+
故:有界规模内给精确行数(count_jsonl 流式、内存恒定);超阈值只读元数据
|
|
2369
|
+
+ 尾部采样估算,**显式标注 exact=False 不假装精确**。要精确值走离线
|
|
2370
|
+
count_jsonl 或审计日志分片轮转(属方向性决策,待定夺)。
|
|
2371
|
+
"""
|
|
2372
|
+
try:
|
|
2373
|
+
st = os.stat(path)
|
|
2374
|
+
except OSError:
|
|
2375
|
+
return {"bytes": 0, "mtime": None, "events": 0, "exact": True}
|
|
2376
|
+
size = st.st_size
|
|
2377
|
+
out = {"bytes": size, "mtime": st.st_mtime}
|
|
2378
|
+
if size <= self.AUDIT_COUNT_MAX_BYTES:
|
|
2379
|
+
out.update({"events": count_jsonl(path), "exact": True})
|
|
2380
|
+
return out
|
|
2381
|
+
est = None
|
|
2382
|
+
try:
|
|
2383
|
+
with open(path, "rb") as f:
|
|
2384
|
+
f.seek(max(0, size - est_sample))
|
|
2385
|
+
tail = f.read()
|
|
2386
|
+
lines = [ln for ln in tail.split(b"\n") if ln.strip()]
|
|
2387
|
+
if lines:
|
|
2388
|
+
est = int(size / (len(tail) / float(len(lines))))
|
|
2389
|
+
except OSError:
|
|
2390
|
+
pass
|
|
2391
|
+
out.update({
|
|
2392
|
+
"events": est, "exact": False,
|
|
2393
|
+
"note": ("超过 %.0f MB 不给全量精确计数(O(n) 磁盘 IO 与量级体检不匹配);"
|
|
2394
|
+
"events 为尾部 %d KB 采样的估算值"
|
|
2395
|
+
% (self.AUDIT_COUNT_MAX_BYTES / 1048576.0, est_sample >> 10))})
|
|
2396
|
+
return out
|
|
2397
|
+
|
|
1950
2398
|
def health_os(self):
|
|
1951
2399
|
h = self.health()
|
|
2400
|
+
audit = self.audit_scale()
|
|
1952
2401
|
h["os"] = {
|
|
1953
2402
|
"roles": self._role_counts(),
|
|
1954
2403
|
"review_pending": len(self.review_list()),
|
|
1955
2404
|
"review_records": len(self.review_records()),
|
|
1956
|
-
|
|
1957
|
-
|
|
2405
|
+
# 审计/墓碑面走 _log_scale(O(1) 量级读数)而非 list(read_jsonl(...))
|
|
2406
|
+
# 也不再全量流式数行:物化读让只读体检把整条通道拖死(实测 4.0 GB
|
|
2407
|
+
# 日志 → RSS 5 GB+、数十分钟不返回),而全量流式扫仍要 O(n) 磁盘 IO
|
|
2408
|
+
# 且日志无上界增长(~2 条/s)→ 随运行时长线性劣化。见 _log_scale。
|
|
2409
|
+
"tombstones": self._log_scale(self.deletions_log)["events"],
|
|
2410
|
+
"audit_events": audit["events"], # 超阈值时为估算值
|
|
2411
|
+
"audit_events_exact": audit["exact"], # False = 上面是估算,非精确
|
|
2412
|
+
"audit_bytes": audit["bytes"], # 量级看这个(O(1) 精确)
|
|
2413
|
+
"audit_events_note": audit.get("note"),
|
|
2414
|
+
# 轮转面(有界性证据):活动文件 ≤rotate_bytes、分片 ≤keep_shards
|
|
2415
|
+
"audit_shards": audit["shards"], # 归档分片数
|
|
2416
|
+
"audit_total_bytes": audit["total_bytes"], # 活动 + 全部分片
|
|
2417
|
+
"audit_total_events": audit["total_events"], # 全量条数(分片走索引缓存)
|
|
2418
|
+
"audit_total_exact": audit["total_exact"], # False = 含超大分片的估算
|
|
2419
|
+
"audit_oversized": audit["oversized"], # 超大历史分片数(待离线切分)
|
|
1958
2420
|
"reflections": len(self.last_d_records()),
|
|
1959
2421
|
# 七件套覆盖度(第 5 篇):目标槽 + 近期事件窗口
|
|
1960
2422
|
"goals": {"total": len(self.list_goals()),
|
|
@@ -2632,9 +3094,9 @@ class MdCGSecure(MdCGOS):
|
|
|
2632
3094
|
return super().clear_recent()
|
|
2633
3095
|
|
|
2634
3096
|
def _candidates(self, layer=None, roles=None, include_work=False,
|
|
2635
|
-
session=None):
|
|
3097
|
+
session=None, branch=None):
|
|
2636
3098
|
out = super()._candidates(layer=layer, roles=roles, include_work=include_work,
|
|
2637
|
-
session=session)
|
|
3099
|
+
session=session, branch=branch)
|
|
2638
3100
|
return [e for e in out if self._readable(e)]
|
|
2639
3101
|
|
|
2640
3102
|
def _neg_coverage(self, terms):
|