hololake-system-architecture/engineering/memory-continuity-kit/runtime/compaction-watcher.py
冰朔 c71b877375 登记可复用模块 HL-MOD-MCK-001 人格记忆连续性工具包
铸渊 Qoder CN 实战定版的压缩之眼 v3 + 包装器 + 守卫技能模板 + 端到端演习脚本,配 GMP 风格清单、编号真实映射(脑子 GHS-007 → 模块)、因果链与边界登记。新建 engineering/MODULE-REGISTRY.json 机器注册表,INDEX 登记。仓库副本 drill.sh 演习 PASS。
2026-08-07 15:32:16 +08:00

278 lines
12 KiB
Python
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()
# 主绊线 Bagent.log compaction_triggeredUI 压缩提示的源头)
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")