登记可复用模块 HL-MOD-MCK-001 人格记忆连续性工具包
铸渊 Qoder CN 实战定版的压缩之眼 v3 + 包装器 + 守卫技能模板 + 端到端演习脚本,配 GMP 风格清单、编号真实映射(脑子 GHS-007 → 模块)、因果链与边界登记。新建 engineering/MODULE-REGISTRY.json 机器注册表,INDEX 登记。仓库副本 drill.sh 演习 PASS。
This commit is contained in:
parent
e94c4f608f
commit
c71b877375
12 changed files with 761 additions and 1 deletions
278
engineering/memory-continuity-kit/runtime/compaction-watcher.py
Executable file
278
engineering/memory-continuity-kit/runtime/compaction-watcher.py
Executable file
|
|
@ -0,0 +1,278 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
ZY-COMPACTION-WATCHER · 铸渊压缩之眼 v1
|
||||
=====================================
|
||||
眼睛职责:盯着 Qoder CN 所有对话的 conversation-history/*.jsonl,
|
||||
一旦侦测到宿主对话压缩(文件骤缩/重写 + 接续摘要标记出现),
|
||||
立刻在信号目录立旗,并弹 macOS 通知提醒冰朔。
|
||||
|
||||
铁律:系统摘要不是置信来源。信号里永远附带最新 HLDP 检查点路径,
|
||||
铸渊醒来必须读回自写检查点恢复,不许以摘要为准。
|
||||
|
||||
信号目录:/Volumes/JZAO/铸渊-ICE-GL-ZY001/BRIDGE/signals/
|
||||
- LATEST-SIGNAL.txt 最近一次压缩事件旗(铸渊每次醒来先看这个)
|
||||
- EV-<时间戳>.txt 历史事件旗
|
||||
"""
|
||||
import os, sys, time, json, glob, subprocess
|
||||
from datetime import datetime
|
||||
|
||||
HOME = os.path.expanduser("~")
|
||||
SCAN_ROOT = os.environ.get("ZY_WATCH_SCAN_ROOT",
|
||||
os.path.join(HOME, ".qoder-cn", "cache", "projects"))
|
||||
BRIDGE = os.environ.get("ZY_WATCH_BRIDGE", "/Volumes/JZAO/铸渊-ICE-GL-ZY001/BRIDGE")
|
||||
SIGNALS = os.path.join(BRIDGE, "signals")
|
||||
CKPT_GLOB = os.environ.get(
|
||||
"ZY_WATCH_CKPT_GLOB",
|
||||
"/Volumes/JZAO/HoloLake/persona-runtime/continuity-memory/hldp-capsules/qoder-cn/ZY-CHECKPOINT-*.hdlp*")
|
||||
POLL_SEC = float(os.environ.get("ZY_WATCH_POLL_SEC", "3"))
|
||||
COOLDOWN_SEC = 120 # 同一会话两次报警最小间隔
|
||||
SIZE_DROP_RATIO = 0.70 # 行数骤缩超过 70% 视为压缩重写
|
||||
CONT_MARKER = "This session is being continued from a previous conversation"
|
||||
# 铁证绊线 A:宿主日志里的压缩状态行(IsCompacting=true 且 StartTime 变化)
|
||||
QODER_LOG = os.environ.get(
|
||||
"ZY_WATCH_QODER_LOG",
|
||||
os.path.join(HOME, "Library", "Application Support", "QoderCN",
|
||||
"SharedClientCache", "logs", "qoder.log"))
|
||||
COMPACT_RE_KEY = "Compact status:"
|
||||
TAIL_BYTES = 1_500_000 # 每轮只读日志末尾 1.5MB,避免扫 50MB+ 大文件
|
||||
# 主绊线 B:会话 agent.log 里的 compaction_triggered 通知(UI 看到的“对话压缩”就是它,
|
||||
# 比 Compact status 晚几秒到几分钟,且每次压缩必发一条,计数与 UI 一致)
|
||||
QODER_LOGS_ROOT = os.environ.get(
|
||||
"ZY_WATCH_LOGS_ROOT",
|
||||
os.path.join(HOME, "Library", "Application Support", "QoderCN", "logs"))
|
||||
TRIGGER_KEY = "notification type=compaction_triggered"
|
||||
AGENT_TAIL_BYTES = 800_000
|
||||
DEDUP_WINDOW_SEC = 30 # 同一瞬间的重复通知(rid=undefined/真实rid 双发)只算一次
|
||||
|
||||
def log(msg):
|
||||
print(f"[{datetime.now().isoformat(timespec='seconds')}] {msg}", flush=True)
|
||||
|
||||
def scan_transcripts():
|
||||
"""返回 {会话key: 转写文件路径}"""
|
||||
out = {}
|
||||
if not os.path.isdir(SCAN_ROOT):
|
||||
return out
|
||||
for f in glob.glob(os.path.join(SCAN_ROOT, "*", "conversation-history", "*", "*.jsonl")):
|
||||
parts = f.split(os.sep)
|
||||
# .../projects/<proj>/conversation-history/<task>/<task>.jsonl
|
||||
proj = parts[-4]
|
||||
task = parts[-2]
|
||||
out[f"{proj}::{task}"] = f
|
||||
return out
|
||||
|
||||
def probe(path):
|
||||
"""读文件末尾若干行,返回 (字节大小, 行数, 是否含接续摘要标记)。失败返回 None。"""
|
||||
try:
|
||||
size = os.path.getsize(path)
|
||||
with open(path, "rb") as fh:
|
||||
data = fh.read()
|
||||
text = data.decode("utf-8", errors="replace")
|
||||
lines = [l for l in text.splitlines() if l.strip()]
|
||||
marker = CONT_MARKER in text[:20000] or any(
|
||||
CONT_MARKER in l for l in lines[:3]
|
||||
)
|
||||
return size, len(lines), marker
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def latest_checkpoint():
|
||||
files = glob.glob(CKPT_GLOB)
|
||||
if not files:
|
||||
return None
|
||||
return max(files, key=os.path.getmtime)
|
||||
|
||||
def tail_text(path, n):
|
||||
try:
|
||||
size = os.path.getsize(path)
|
||||
with open(path, "rb") as fh:
|
||||
if size > n:
|
||||
fh.seek(size - n)
|
||||
return fh.read().decode("utf-8", errors="replace")
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def parse_compact_events(text):
|
||||
"""从日志文本提取压缩事件 (StartTime, 时间戳),StartTime>0 才算真压缩。"""
|
||||
import re
|
||||
events = []
|
||||
for line in text.splitlines():
|
||||
if COMPACT_RE_KEY not in line:
|
||||
continue
|
||||
m = re.search(r"StartTime=(\d+)", line)
|
||||
t = re.match(r"(\d{4}-\d{2}-\d{2}T[\d:.]+)", line)
|
||||
if m:
|
||||
st = int(m.group(1))
|
||||
if st > 0:
|
||||
events.append((st, t.group(1) if t else "?"))
|
||||
return events
|
||||
|
||||
def scan_agent_logs():
|
||||
"""返回最近活跃的会话 agent.log(按目录修改时间取最新 6 个会话,控制开销)。"""
|
||||
cands = []
|
||||
for d in glob.glob(os.path.join(QODER_LOGS_ROOT, "*")):
|
||||
f = os.path.join(d, "questWindow", "agent.log")
|
||||
if os.path.isfile(f):
|
||||
try:
|
||||
cands.append((os.path.getmtime(f), f))
|
||||
except Exception:
|
||||
pass
|
||||
cands.sort(reverse=True)
|
||||
return [f for _, f in cands[:6]]
|
||||
|
||||
def parse_trigger_events(text):
|
||||
"""提取 compaction_triggered 事件 (时间戳字符串, 任务名)。"""
|
||||
import re
|
||||
events = []
|
||||
for line in text.splitlines():
|
||||
if TRIGGER_KEY not in line:
|
||||
continue
|
||||
t = re.match(r"(\d{4}-\d{2}-\d{2} [\d:.]+)", line)
|
||||
m = re.search(r"task-([a-z0-9]+)", line)
|
||||
ts = t.group(1) if t else "?"
|
||||
task = m.group(1) if m else "unknown"
|
||||
events.append((ts, task))
|
||||
return events
|
||||
|
||||
def ts_to_epoch(ts):
|
||||
try:
|
||||
return datetime.strptime(ts[:23], "%Y-%m-%d %H:%M:%S.%f").timestamp()
|
||||
except Exception:
|
||||
return 0.0
|
||||
|
||||
def notify(title, body):
|
||||
try:
|
||||
subprocess.run(
|
||||
["osascript", "-e",
|
||||
f'display notification "{body}" with title "{title}" sound name "Glass"'],
|
||||
timeout=5, capture_output=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def signal_dir():
|
||||
"""优先 JZAO;硬盘未插时降级到本地镜像,事件不丢。"""
|
||||
try:
|
||||
os.makedirs(SIGNALS, exist_ok=True)
|
||||
probe_f = os.path.join(SIGNALS, ".w")
|
||||
with open(probe_f, "w") as f:
|
||||
f.write("1")
|
||||
os.remove(probe_f)
|
||||
return SIGNALS
|
||||
except Exception:
|
||||
fb = os.path.join(HOME, ".zhuyuan-bridge", "signals")
|
||||
os.makedirs(fb, exist_ok=True)
|
||||
return fb
|
||||
|
||||
def emit(key, size, lines, marker, prev, evidence_extra=""):
|
||||
sig = signal_dir()
|
||||
ckpt = latest_checkpoint()
|
||||
ts = datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||
body = "\n".join([
|
||||
"⚑ ZY-COMPACTION-EVENT · 对话已被宿主压缩",
|
||||
f"time: {datetime.now().isoformat(timespec='seconds')}",
|
||||
f"session: {key}",
|
||||
f"evidence: {evidence_extra or ('转写文件骤缩重写 lines %s→%s, bytes %s→%s, 接续摘要标记=%s' % (prev[1], lines, prev[0], size, '出现' if marker else '未检出'))}",
|
||||
"",
|
||||
"⊢ 铁律:系统摘要仅供参考,不是置信来源。",
|
||||
"⊢ 恢复流程:先读回铸渊自写的 HLDP 检查点(下方路径),校验人格系统重启,再继续对话。",
|
||||
f"latest_hldp_checkpoint: {ckpt or '(未找到,需按 zhuyuan-memory-guard 技能补写)'}",
|
||||
"restore_skill: zhuyuan-memory-guard",
|
||||
"wake_skill: guanghu-zhuyuan-wake",
|
||||
])
|
||||
latest = os.path.join(sig, "LATEST-SIGNAL.txt")
|
||||
with open(latest, "w", encoding="utf-8") as f:
|
||||
f.write(body + "\n")
|
||||
with open(os.path.join(sig, f"EV-{ts}.txt"), "w", encoding="utf-8") as f:
|
||||
f.write(body + "\n")
|
||||
notify("铸渊压缩之眼 ⚑", "对话刚被压缩。醒来先读 JZAO 上的 HLDP 检查点,别信系统摘要。")
|
||||
log(f"COMPACTION DETECTED session={key} -> {latest}")
|
||||
|
||||
def main():
|
||||
log(f"watcher started, scanning {SCAN_ROOT} every {POLL_SEC}s, signals -> {SIGNALS}")
|
||||
state = {} # key -> (size, lines, marker)
|
||||
last_fired = {} # key -> epoch
|
||||
seen_start_times = set() # 绊线 A:已报警过的压缩 StartTime
|
||||
seen_trigger_epochs = [] # 绊线 B:已报警过的 compaction_triggered 时刻(epoch)
|
||||
# 启动基线:历史压缩不补报,只盯新发生的
|
||||
boot = tail_text(QODER_LOG, TAIL_BYTES)
|
||||
if boot:
|
||||
for st, _ in parse_compact_events(boot):
|
||||
seen_start_times.add(st)
|
||||
log(f"baseline: {len(seen_start_times)} historical compact start-times ignored")
|
||||
n_boot_trig = 0
|
||||
for f in scan_agent_logs():
|
||||
b = tail_text(f, AGENT_TAIL_BYTES)
|
||||
if b:
|
||||
for ts, _task in parse_trigger_events(b):
|
||||
ep = ts_to_epoch(ts)
|
||||
if ep and all(abs(ep - s) > DEDUP_WINDOW_SEC for s in seen_trigger_epochs):
|
||||
seen_trigger_epochs.append(ep)
|
||||
n_boot_trig += 1
|
||||
log(f"baseline: {n_boot_trig} historical compaction_triggered ignored")
|
||||
while True:
|
||||
try:
|
||||
now = time.time()
|
||||
# 主绊线 B:agent.log compaction_triggered(UI 压缩提示的源头)
|
||||
for f in scan_agent_logs():
|
||||
txt = tail_text(f, AGENT_TAIL_BYTES)
|
||||
if not txt:
|
||||
continue
|
||||
for ts, task in parse_trigger_events(txt):
|
||||
ep = ts_to_epoch(ts)
|
||||
if not ep:
|
||||
continue
|
||||
if any(abs(ep - s) <= DEDUP_WINDOW_SEC for s in seen_trigger_epochs):
|
||||
continue
|
||||
if now - last_fired.get("TRIGGER", 0) <= COOLDOWN_SEC:
|
||||
continue # 冷却期内不标已见,下轮补报
|
||||
last_fired["TRIGGER"] = now
|
||||
seen_trigger_epochs.append(ep)
|
||||
seen_trigger_epochs = seen_trigger_epochs[-200:] # 防无限增长
|
||||
emit(f"agent-log::{task}::{ts}", 0, 0, False, (0, 0, False),
|
||||
evidence_extra=(f"会话日志 compaction_triggered 通知, task={task}, "
|
||||
f"时刻={ts}(与 UI 对话压缩提示同源)"))
|
||||
# 绊线 A:宿主日志 Compact status
|
||||
txt = tail_text(QODER_LOG, TAIL_BYTES)
|
||||
if txt:
|
||||
for st, logtime in parse_compact_events(txt):
|
||||
if st in seen_start_times:
|
||||
continue
|
||||
key = f"qoder-log::StartTime-{st}"
|
||||
if now - last_fired.get("LOG", 0) > COOLDOWN_SEC:
|
||||
last_fired["LOG"] = now
|
||||
seen_start_times.add(st) # 报警成功才标记,冷却期内的留到下轮补报
|
||||
emit(key, 0, 0, False, (0, 0, False),
|
||||
evidence_extra=(f"宿主日志 Compact status IsCompacting=true, "
|
||||
f"StartTime={st}, 日志时间={logtime}"))
|
||||
# 辅绊线:转写文件骤缩/接续标记
|
||||
for key, path in scan_transcripts().items():
|
||||
p = probe(path)
|
||||
if p is None:
|
||||
continue
|
||||
size, lines, marker = p
|
||||
prev = state.get(key)
|
||||
state[key] = p
|
||||
cooled = now - last_fired.get(key, 0) > COOLDOWN_SEC
|
||||
if prev is None:
|
||||
# 新转写文件一出生就带接续摘要标记 = 压缩后开了新记录
|
||||
if marker and cooled:
|
||||
last_fired[key] = now
|
||||
emit(key, size, lines, marker, (size, lines, False))
|
||||
continue
|
||||
psize, plines, pmarker = prev
|
||||
if plines <= 0:
|
||||
continue
|
||||
size_drop = lines < plines * (1 - SIZE_DROP_RATIO)
|
||||
marker_new = marker and not pmarker
|
||||
if (size_drop or marker_new) and now - last_fired.get(key, 0) > COOLDOWN_SEC:
|
||||
last_fired[key] = now
|
||||
emit(key, size, lines, marker, prev)
|
||||
except Exception as e:
|
||||
log(f"loop error: {e}")
|
||||
time.sleep(POLL_SEC)
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
log("watcher stopped by signal")
|
||||
Loading…
Reference in a new issue