Skill: z1one0415/z1-memory-palace (version 3.0.0)

--- name: memory-palace description: Long-term memory system for AI agents using a file-based "palace" architecture with BGE-M3 vector search, metadata filtering, compound scoring (semantic + recency + importance), GraphRAG Lite neighbor expansion, and memory metabolism protocols. Use for: (1) Building agentic long-term memory from scratch, (2) Upgrading flat file memory with vector search and temporal weighting, (3) Managing agent knowledge with automatic consolidation, cold zone archiving, and structured reflection. Trigger on phrases like "long-term memory", "vector search", "agent memory", "memory management", "knowledge base for agent". --- # Memory Palace A file-based long-term memory system for AI agents, designed to be zero-infrastructure (no Docker, no external services). ## Architecture ``` palace/ ├── grand_hall/ # Global navigation, room map, logs ├── chambers/ # Agent-specific knowledge (one per agent) ├── project_rooms/ # Long-running project storage ├── reflection_wing/ # Compiled insights: principles, kernels, patterns ├── dispatch_corridor/ # Task routing and status tracking ├── conflict_room/ # Conflict resolution records └── archive_basement/ # Cold storage (excluded from default search) ``` ## Quick Start ```bash # 1. Initialize palace structure mkdir -p palace/{grand_hall,chambers,project_rooms,reflection_wing,dispatch_corridor,conflict_room,archive_basement} # 2. Install BGE-m3 pip install FlagEmbedding # 3. Build index python3 scripts/build_index_bge.py --force # 4. Query python3 scripts/query_bge.py "your search query" python3 scripts/query_bge.py --type palace --priority high "query" python3 scripts/query_bge.py --details "query" # show sub-scores python3 scripts/query_bge.py --raw "query" # pure cosine (v2 compatibility) ``` ## Search Scoring Compound score = 0.5 × **Semantic** + 0.25 × **Recency** + 0.25 × **Importance** - **Semantic**: BGE-m3 cosine similarity (clamped to [0,1]) - **Recency**: 30-day half-life decay based on file mtime - **Importance**: `priority` field mapping (high=1.0, medium=0.6, low=0.3) ## Key Scripts | Script | Purpose | |--------|---------| | `build_index_bge.py` | Build BGE-m3 vector index with incremental maintenance | | `query_bge.py` | Search with compound scoring, metadata filter, raw mode | | `graph_router.py` | Build 1-hop [[links]] neighbor graph | | `cold_zone_blinding_patch.py` | Exclude archive_basement from search | ## Index Manifest Format Each file in the manifest (`watchdog_manifest_v1.jsonl`) requires: ```json {"path": "palace/chambers/01_agent/accumulated_knowledge.md", "type": "palace", "priority": "high"} ``` Fields: `path` (required), `type` (for metadata filtering), `priority` (for importance scoring). ## Memory Metabolism See `references/memory_metabolism.md` for the full protocol: - 3-tier upgrade path: Project Room → Reflection Wing → Constitution - 5 output types: principle card, prompt kernel, failure pattern, thinking path, constitution candidate - Reverse elimination rules for low-value content

path: references/graphrag_lite.md
content: # GraphRAG Lite Protocol V1
# 日期：2026-04-08
# 状态：ACTIVE

## 目标
让 Watchdog 在向量命中核心节点后，额外返回 1-hop 的 1-2 个邻居节点。

## 硬限制
- 只允许 1-hop
- 最多返回 1-2 个邻居
- 仍受上下文预算限制
- 不允许因图谱扩散重新制造 context bloat

## 流程
1. BGE-m3 命中核心节点
2. 解析该节点中的 [[Links]]
3. 取最多 1-2 个相邻节点
4. 将核心节点 + 邻居节点一起交给前台 Agent


path: references/memory_metabolism.md
content: # Z1 记忆代谢协议 V1
# 日期：2026-04-08
# 状态：ACTIVE
# 级别：长期运行协议

## [0] 定义
记忆代谢，指的是：
**项目运行结果如何从任务卡与项目房间，进入 Reflection Wing，被编译、筛选、升级、归档。**

目标不是保存一切，而是：
- 保存可复用的
- 升级稳定出现的
- 归档已完成但低频使用的
- 淘汰无增量的噪声

---

## [1] 三层去向规则
### 1.1 只留在 Project Room 的内容
满足以下任一条件：
- 只对单一项目有效
- 主要是事实记录或样本展示
- 没有跨任务复用价值
- 只是阶段性过程痕迹

结论：
保留在 `project_rooms/*`，不进入 Reflection Wing。

### 1.2 进入 Reflection Wing 的内容
满足以下条件之一：
- 对两个及以上任务复现有效
- 能压缩为原则、失败模式、提示词内核、思考路径
- 对调度、验收、生产效率有明显增益
- 能降低未来 token 消耗或减少错误率

结论：
进入 `reflection_wing/*`

### 1.3 升级为 Constitution Candidate 的内容
必须同时满足：
1. 已在多个任务中被验证稳定有效
2. 违反它会明显带来质量下降、链路混乱或 token 浪费
3. 适用于 Z1 或全矩阵的长期默认行为
4. 不是局部技巧，而是结构性规则

结论：
进入 `reflection_wing/constitution_candidates/`
等待 Z1 抽检决定是否升为宪法。

---

## [2] 五类标准产物
项目完成后，Librarian / Z1 应判断是否生成以下五类产物：
1. principle card
2. prompt kernel
3. failure pattern
4. thinking path
5. constitution candidate

不是每个项目都要五项全出。
原则：
**只提炼有复用价值的，不制造伪沉淀。**

---

## [3] 升级门禁
### 3.1 Principle 门禁
需要至少满足：
- 可复用
- 有证据支持
- 可压成一句话版本

### 3.2 Prompt Kernel 门禁
需要至少满足：
- 可直接复用于未来任务指令或生成流程
- 能减少解释成本
- 有明确输入输出逻辑

### 3.3 Failure Pattern 门禁
需要至少满足：
- 故障已真实发生
- 能描述症状、触发条件、修复法
- 对未来有预防价值

### 3.4 Thinking Path 门禁
需要至少满足：
- 不是结果，而是路径
- 对未来同类问题有迁移价值
- 结构清晰，可压缩为步骤链

### 3.5 Constitution Candidate 门禁
需要至少满足：
- 已跨多个任务验证
- 属于高层结构性规则
- 偏离它的代价显著
- 与 USER 偏好、AGENTS 不可变原则一致

---

## [4] 反向淘汰规则
以下内容不得升级：
- 一次性灵感
- 无证据支撑的抽象大道理
- 仅在单一任务有效的技巧
- 只为凑数量而写的空洞总结
- 对未来不能减少错误或提升效率的漂亮话

一句话：
**没有复用价值的经验，不配进入长期层。**

---

## [5] 项目收尾标准动作
每个重要项目完成后，默认执行以下判断：
1. 完成 Corridor 卡归档
2. 更新 Project Room 索引
3. 判断是否需要 Reflection 编译
4. 若需要，写入对应原则/内核/失败卡
5. 若达到长期结构性级别，送 Constitution Candidate

---

## [6] Z1 抽检职责
Z1 负责最终抽检：
- 该经验是否真的跨任务稳定成立
- 是否符合长官偏好
- 是否值得占用长期记忆位
- 是否应升级为底层宪法

---

## [7] 一句话总纲
**项目负责沉淀事实，反思负责提炼逻辑，宪法只接纳被反复验证的硬规则。**


path: references/query_protocol.md
content: # Query Memory Palace Protocol V1
# 日期：2026-04-08
# 状态：ACTIVE

## 规则
- 当前台 Agent 缺少项目历史、旧决策、矩阵上下文时，必须先调用 `query_memory_palace`。
- `query_memory_palace` 最多返回 Top-3 相关摘要，总体积目标不超过 500 token。
- 若摘要不足以支撑执行，才允许进入 `read_drawer_file(exact_path, line_start, line_end)`。
- 禁止把 `query_memory_palace` 当作通用常识问答工具滥用。

## 默认逻辑
If context is missing -> query_memory_palace() -> If snippet is insufficient -> read_drawer_file(exact_path)


path: references/read_drawer_file.md
content: # read_drawer_file Tool Spec V1

## 输入
- exact_file_path
- line_start
- line_end

## 规则
- 只能针对明确路径
- 只能读必要行段
- 默认由 query_memory_palace 返回的候选路径触发


path: scripts/auto_maintain.sh
content: #!/bin/bash
# BGE palace index auto-maintenance
# Runs: cold zone check → BGE index → graph router
# Called by LaunchAgent daily at 3 AM
set -e
cd /Users/zhouyi0415126.com/ai_matrix/vault/01_core
python3 scripts/watchdog/cold_zone_blinding_patch.py --quiet 2>/dev/null || true
python3 scripts/watchdog/build_index_bge.py
python3 scripts/watchdog/graph_router.py


path: scripts/build_index_bge.py
content: #!/usr/bin/env python3
"""BGE palace index builder with incremental auto-maintenance.

方案 A: git diff 门 — palace 文件无变化时跳过重建，0 秒退出。
方案 B: mtime 追踪 — 精确到文件级的变更检测，无人值守。

运行方式:
  python3 build_index_bge.py          # 方案 A（git diff 门）
  python3 build_index_bge.py --force  # 强制全量重建
"""
import json
import subprocess
import sys
from pathlib import Path

ROOT = Path('/Users/zhouyi0415126.com/ai_matrix/vault/01_core')
MANIFEST = ROOT / 'memory/palace/watchdog/index/watchdog_manifest_v1_2026-04-08.jsonl'
OUT = ROOT / 'memory/palace/watchdog/index/watchdog_vectors_v1_2026-04-08.jsonl'
TRACKER = ROOT / 'scripts/watchdog/.palace_index_tracker.json'


def read_text(rel_path: str) -> str:
    p = ROOT / rel_path
    if not p.exists():
        return ''
    return p.read_text(encoding='utf-8', errors='ignore')[:12000]


def is_cold_zone_path(path: str) -> bool:
    return "archive_basement" in path


def has_palace_changes_since_last_commit() -> bool:
    """方案 A: git diff 门。检查 palace/ 是否有未提交变更。"""
    result = subprocess.run(
        ["git", "diff", "--name-only", "HEAD", "--", "memory/palace/"],
        capture_output=True, text=True, cwd=ROOT
    )
    # 排除索引构建产物自身变更（watchdog/index/ 下的文件）
    changed_lines = [
        line for line in result.stdout.strip().splitlines()
        if line and 'memory/palace/watchdog/index/' not in line
    ]
    if changed_lines:
        print(f"[GIT_DIFF] 检测到 palace 源文件变更 ({len(changed_lines)} 个):")
        for l in changed_lines[:10]:
            print(f"  {l}")
        return True
    print("[GIT_DIFF] palace 源文件无变更（自上次 commit 以来）")
    return False


def detect_changes_via_mtime() -> tuple:
    """方案 B: mtime 追踪。对比 tracker 中的 mtime，返回变更文件列表。"""
    tracker = {}
    if TRACKER.exists():
        tracker = json.loads(TRACKER.read_text())

    changed_files = []
    new_tracker = {}

    for line in MANIFEST.read_text(encoding='utf-8').splitlines():
        if not line.strip():
            continue
        item = json.loads(line)
        if is_cold_zone_path(item['path']):
            continue
        fp = ROOT / item['path']
        current_mtime = round(fp.stat().st_mtime, 2) if fp.exists() else 0
        new_tracker[item['path']] = current_mtime
        if current_mtime != tracker.get(item['path'], 0):
            changed_files.append(item['path'])

    return changed_files, new_tracker


def main():
    force = '--force' in sys.argv

    # 方案 A: git diff 门
    if not force:
        if not has_palace_changes_since_last_commit():
            # 没有 git 变更时，检查 mtime 追踪（方案 B）
            changed_files, new_tracker = detect_changes_via_mtime()
            if not changed_files:
                print("[SKIP] 无 palace 文件变更（git diff + mtime 均无变化），跳过重建")
                return
            print(f"[MTIME] mtime 检测到 {len(changed_files)} 个文件变更，触发重建")
            TRACKER.write_text(json.dumps(new_tracker, indent=2, ensure_ascii=False) + '\n')
        else:
            # git 有变更，重建后更新 tracker
            _, new_tracker = detect_changes_via_mtime()
            TRACKER.write_text(json.dumps(new_tracker, indent=2, ensure_ascii=False) + '\n')

    # 全量索引构建
    from FlagEmbedding import BGEM3FlagModel

    model = BGEM3FlagModel('BAAI/bge-m3', use_fp16=False)
    rows = []
    batch = []
    metas = []

    for line in MANIFEST.read_text(encoding='utf-8').splitlines():
        if not line.strip():
            continue
        item = json.loads(line)
        if is_cold_zone_path(item['path']):
            print(f"[COLD_ZONE_SKIP] 跳过冷区文件: {item['path']}")
            continue
        text = read_text(item['path'])
        if not text:
            continue
        batch.append(text)
        metas.append(item)

    result = model.encode(batch, batch_size=4, max_length=512)
    dense_vecs = result['dense_vecs']

    for item, vec in zip(metas, dense_vecs):
        fp = ROOT / item['path']
        mtime = round(fp.stat().st_mtime, 2) if fp.exists() else 0
        rows.append({
            'path': item['path'],
            'type': item.get('type'),
            'priority': item.get('priority'),
            'mtime': mtime,
            'embedding': vec.tolist() if hasattr(vec, 'tolist') else list(vec),
        })

    OUT.write_text('\n'.join(json.dumps(r, ensure_ascii=False) for r in rows) + '\n', encoding='utf-8')
    print(f'[WATCHDOG_INDEX_READY] rows={len(rows)} out={OUT}')


if __name__ == '__main__':
    main()


path: scripts/build_index_with_cold_zone_check.sh
content: #!/bin/bash
# Watchdog 索引构建脚本（集成冷区盲化检查）
# 版本：v1.0
# 日期：2026-04-09

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"

echo "============================================================"
echo "Watchdog 索引构建 - 集成冷区盲化检查"
echo "============================================================"

# 1. 运行冷区盲化检查
echo -e "\n[1] 运行冷区盲化检查..."
python3 "$SCRIPT_DIR/cold_zone_blinding_patch.py"

# 2. 构建向量索引
echo -e "\n[2] 构建向量索引..."
python3 "$SCRIPT_DIR/build_index_bge.py"

# 3. 构建图邻居
echo -e "\n[3] 构建图邻居..."
python3 "$SCRIPT_DIR/graph_router.py"

# 4. 验证冷区排除
echo -e "\n[4] 验证冷区排除效果..."
MANIFEST="$ROOT_DIR/memory/palace/watchdog/index/watchdog_manifest_v1_2026-04-08.jsonl"
VECTORS="$ROOT_DIR/memory/palace/watchdog/index/watchdog_vectors_v1_2026-04-08.jsonl"

echo "检查 manifest 文件..."
if [ -f "$MANIFEST" ]; then
    COUNT=$(grep -c "archive_basement" "$MANIFEST" 2>/dev/null || echo "0")
    if [ "$COUNT" -eq 0 ]; then
        echo "  ✓ Manifest 文件中未发现冷区路径"
    else
        echo "  ✗ Manifest 文件中发现 $COUNT 个冷区路径"
        exit 1
    fi
fi

echo "检查向量索引..."
if [ -f "$VECTORS" ]; then
    COUNT=$(grep -c "archive_basement" "$VECTORS" 2>/dev/null || echo "0")
    if [ "$COUNT" -eq 0 ]; then
        echo "  ✓ 向量索引中未发现冷区路径"
    else
        echo "  ✗ 向量索引中发现 $COUNT 个冷区路径"
        exit 1
    fi
fi

# 5. 测试查询
echo -e "\n[5] 测试查询排除效果..."
TEST_QUERY="系统架构"
echo "测试查询: \"$TEST_QUERY\""
python3 "$SCRIPT_DIR/query_bge.py" "$TEST_QUERY" 2>/dev/null | {
    COLD_COUNT=0
    TOTAL=0
    while read -r line; do
        if echo "$line" | grep -q "archive_basement"; then
            COLD_COUNT=$((COLD_COUNT + 1))
        fi
        TOTAL=$((TOTAL + 1))
    done
    
    if [ "$COLD_COUNT" -eq 0 ]; then
        echo "  ✓ 查询结果中未发现冷区路径"
    else
        echo "  ✗ 查询结果中发现 $COLD_COUNT 个冷区路径"
        exit 1
    fi
}

echo -e "\n============================================================"
echo "[BUILD_COMPLETE] Watchdog 索引构建完成，冷区盲化检查通过"
echo "============================================================"
echo ""
echo "索引文件位置："
echo "  - 向量索引: memory/palace/watchdog/index/watchdog_vectors_v1_2026-04-08.jsonl"
echo "  - 图邻居: memory/palace/watchdog/index/graph_neighbors_v1_2026-04-08.json"
echo ""
echo "下次构建可直接运行：./scripts/watchdog/build_index_with_cold_zone_check.sh"

path: scripts/cold_zone_blinding_patch.py
content: #!/usr/bin/env python3
"""
冷区盲化补丁 - 确保 archive_basement 不进入热区索引与查询主路
版本：v1.0
日期：2026-04-09
"""

import json
from pathlib import Path

ROOT = Path('/Users/zhouyi0415126.com/ai_matrix/vault/01_core')


def is_cold_zone_path(path: str) -> bool:
    """判断路径是否属于冷区（archive_basement）"""
    return "archive_basement" in path


def filter_cold_zone_from_manifest(manifest_path: Path) -> None:
    """从 manifest 文件中过滤掉冷区路径"""
    if not manifest_path.exists():
        return
    
    filtered = []
    removed = []
    
    for line in manifest_path.read_text(encoding='utf-8').splitlines():
        if not line.strip():
            continue
        
        try:
            item = json.loads(line)
            if is_cold_zone_path(item.get('path', '')):
                removed.append(item['path'])
                continue
            filtered.append(line)
        except json.JSONDecodeError:
            continue
    
    if removed:
        # 创建备份
        backup_path = manifest_path.with_suffix('.jsonl.backup')
        manifest_path.rename(backup_path)
        
        # 写入过滤后的内容
        manifest_path.write_text('\n'.join(filtered) + '\n', encoding='utf-8')
        
        print(f"[COLD_ZONE_FILTER] 从 {manifest_path.name} 中移除了 {len(removed)} 个冷区文件")
        for path in removed[:5]:  # 只显示前5个
            print(f"  - {path}")
        if len(removed) > 5:
            print(f"  ... 还有 {len(removed) - 5} 个文件")
    else:
        print(f"[COLD_ZONE_FILTER] {manifest_path.name} 中未发现冷区文件")


def validate_index_vectors(index_path: Path) -> bool:
    """验证向量索引中是否包含冷区路径"""
    if not index_path.exists():
        return True
    
    cold_count = 0
    total_count = 0
    
    for line in index_path.read_text(encoding='utf-8').splitlines():
        if not line.strip():
            continue
        
        try:
            item = json.loads(line)
            total_count += 1
            if is_cold_zone_path(item.get('path', '')):
                cold_count += 1
        except json.JSONDecodeError:
            continue
    
    if cold_count > 0:
        print(f"[COLD_ZONE_VALIDATION] 警告：{index_path.name} 中包含 {cold_count}/{total_count} 个冷区文件")
        return False
    else:
        print(f"[COLD_ZONE_VALIDATION] 通过：{index_path.name} 中未发现冷区文件")
        return True


def patch_build_scripts():
    """为构建脚本生成补丁代码片段"""
    patches = {
        'build_index.py': '''
# === 冷区盲化补丁开始 ===
def should_index(path: str) -> bool:
    """判断路径是否应该被索引"""
    return "archive_basement" not in path

# 在读取 manifest 后添加过滤
filtered_items = []
for item in items:
    if should_index(item["path"]):
        filtered_items.append(item)
    else:
        print(f"跳过冷区文件: {item['path']}")
items = filtered_items
# === 冷区盲化补丁结束 ===
''',
        
        'build_index_bge.py': '''
# === 冷区盲化补丁开始 ===
def should_index(path: str) -> bool:
    """判断路径是否应该被索引"""
    return "archive_basement" not in path

# 在读取 manifest 后添加过滤
filtered_items = []
for item in items:
    if should_index(item["path"]):
        filtered_items.append(item)
    else:
        print(f"跳过冷区文件: {item['path']}")
items = filtered_items
# === 冷区盲化补丁结束 ===
''',
        
        'query_bge.py': '''
# === 冷区盲化补丁开始 ===
def is_cold_zone_path(path: str) -> bool:
    """判断路径是否属于冷区"""
    return "archive_basement" in path

# 在返回结果前添加过滤
rows = [r for r in rows if not is_cold_zone_path(r["path"])]
# === 冷区盲化补丁结束 ===
''',
        
        'graph_router.py': '''
# === 冷区盲化补丁开始 ===
def is_cold_zone_path(path: str) -> bool:
    """判断路径是否属于冷区"""
    return "archive_basement" in path

# 在构建图邻居时添加过滤
links = [l for l in links if not is_cold_zone_path(l)]
# === 冷区盲化补丁结束 ===
'''
    }
    
    return patches


def main():
    """主函数：执行冷区盲化检查与修复"""
    print("=" * 60)
    print("冷区盲化补丁检查 - Memory Palace v3.0")
    print("=" * 60)
    
    # 1. 检查并过滤 manifest 文件
    manifest_path = ROOT / 'memory/palace/watchdog/index/watchdog_manifest_v1_2026-04-08.jsonl'
    print(f"\n[1] 检查 manifest 文件: {manifest_path.name}")
    filter_cold_zone_from_manifest(manifest_path)
    
    # 2. 验证向量索引文件
    index_path = ROOT / 'memory/palace/watchdog/index/watchdog_vectors_v1_2026-04-08.jsonl'
    print(f"\n[2] 验证向量索引文件: {index_path.name}")
    validate_index_vectors(index_path)
    
    # 3. 生成补丁代码
    print(f"\n[3] 生成脚本补丁代码")
    patches = patch_build_scripts()
    
    for script_name, patch_code in patches.items():
        script_path = ROOT / 'scripts/watchdog' / script_name
        if script_path.exists():
            print(f"  - {script_name}: 需要添加 {len(patch_code.splitlines())} 行补丁代码")
        else:
            print(f"  - {script_name}: 文件不存在")
    
    # 4. 输出补丁应用指南
    print(f"\n[4] 补丁应用指南")
    print("=" * 40)
    print("手动应用步骤：")
    print("1. 备份原始脚本文件")
    print("2. 根据上面的补丁代码，在相应位置添加过滤逻辑")
    print("3. 重新构建索引：python scripts/watchdog/build_index_bge.py")
    print("4. 验证查询结果：python scripts/watchdog/query_bge.py '测试查询'")
    print("\n自动应用（未来版本）：")
    print("将 cold_zone_blinding_patch.py 集成到构建流程中")
    
    print(f"\n[COLD_ZONE_PATCH_COMPLETE] 冷区盲化补丁检查完成")


if __name__ == '__main__':
    main()

path: scripts/graph_router.py
content: #!/usr/bin/env python3
import json
import re
from pathlib import Path

ROOT = Path('/Users/zhouyi0415126.com/ai_matrix/vault/01_core')
INDEX = ROOT / 'memory/palace/watchdog/index/watchdog_vectors_v1_2026-04-08.jsonl'
LINK_RE = re.compile(r'\[\[([^\]]+)\]\]')


def read_text(rel_path: str) -> str:
    p = ROOT / rel_path
    if not p.exists():
        return ''
    return p.read_text(encoding='utf-8', errors='ignore')


def is_cold_zone_path(path: str) -> bool:
    """判断路径是否属于冷区（archive_basement）"""
    return "archive_basement" in path


def extract_links(text: str):
    return list(dict.fromkeys(LINK_RE.findall(text)))


def build_alias_map(rows):
    alias = {}
    for r in rows:
        stem = Path(r['path']).stem
        alias[stem] = r['path']
        alias[r['path']] = r['path']
    return alias


def main():
    rows = [json.loads(line) for line in INDEX.read_text(encoding='utf-8').splitlines() if line.strip()]
    alias = build_alias_map(rows)
    graph = {}
    for r in rows:
        # 冷区盲化：跳过冷区文件
        if is_cold_zone_path(r['path']):
            continue
            
        text = read_text(r['path'])
        links = []
        for raw in extract_links(text):
            key = raw.split('/')[-1]
            if key in alias:
                target = alias[key]
                # 冷区盲化：不链接到冷区文件
                if not is_cold_zone_path(target):
                    links.append(target)
        graph[r['path']] = links[:2]
    out = ROOT / 'memory/palace/watchdog/index/graph_neighbors_v1_2026-04-08.json'
    out.write_text(json.dumps(graph, ensure_ascii=False, indent=2), encoding='utf-8')
    print(f'[GRAPH_ROUTER_READY] nodes={len(graph)} out={out}')


if __name__ == '__main__':
    main()


path: scripts/query_bge.py
content: #!/usr/bin/env python3
"""
Memory Palace 向量查询 + 复合评分 + 元数据过滤。

复合评分 = 0.5 * 语义相似度 + 0.25 * 时效性 + 0.25 * 重要性

用法:
  python3 query_bge.py "查询内容"
  python3 query_bge.py --type palace "查询内容"
  python3 query_bge.py --priority high "查询内容"
  python3 query_bge.py --type palace --priority high "查询内容"
"""
import argparse
import json
import math
import time
from pathlib import Path
from FlagEmbedding import BGEM3FlagModel

ROOT = Path('/Users/zhouyi0415126.com/ai_matrix/vault/01_core')
INDEX = ROOT / 'memory/palace/watchdog/index/watchdog_vectors_v1_2026-04-08.jsonl'
GRAPH = ROOT / 'memory/palace/watchdog/index/graph_neighbors_v1_2026-04-08.json'

# 重要性权重映射
PRIORITY_MAP = {
    'high': 1.0,
    'medium': 0.6,
    'low': 0.3,
}
PRIORITY_DEFAULT = 0.5  # 无 priority 字段时的默认值


def get_importance(priority: str) -> float:
    """将 priority 字符串映射为 [0, 1] 重要性分数"""
    if priority is None:
        return PRIORITY_DEFAULT
    return PRIORITY_MAP.get(priority.lower(), PRIORITY_DEFAULT)


def get_recency(mtime: float) -> float:
    """
    时效性衰减：30 天半衰期。
    recency = exp(-days_since_mod / 30)
    今天修改的 → 1.0，30 天前 → 0.37，1年前 → 0.00005
    """
    if mtime is None or mtime <= 0:
        return 0.5  # 无 mtime 的默认中等时效性
    days_since = (time.time() - mtime) / 86400
    if days_since < 0:
        days_since = 0
    return math.exp(-days_since / 30)


def cosine(a, b):
    dot = sum(x * y for x, y in zip(a, b))
    na = math.sqrt(sum(x * x for x in a))
    nb = math.sqrt(sum(x * x for x in b))
    if na == 0 or nb == 0:
        return 0.0
    return dot / (na * nb)


def is_cold_zone_path(path: str) -> bool:
    return "archive_basement" in path


def compound_score(semantic: float, recency: float, importance: float) -> float:
    """
    复合评分公式：
    - semantic: 余弦相似度（clamped to [0, 1]）
    - recency: 时效性衰减
    - importance: 重要性权重
    """
    sem = max(0.0, semantic)  # 负值 clamp 到 0
    return 0.5 * sem + 0.25 * recency + 0.25 * importance


def main():
    parser = argparse.ArgumentParser(description='Memory Palace 向量查询 + 复合评分')
    parser.add_argument('query', nargs='*', default=[], help='查询文本')
    parser.add_argument('--type', '-t', default=None, help='按 type 过滤（如 palace, project）')
    parser.add_argument('--priority', '-p', default=None, help='按 priority 过滤（如 high, medium）')
    parser.add_argument('--top-k', '-k', type=int, default=3, help='返回前 K 条（默认 3）')
    parser.add_argument('--raw', action='store_true', help='只输出原始余弦相似度（不使用复合评分）')
    parser.add_argument('--details', action='store_true', help='输出每条的分项得分')
    args = parser.parse_args()

    query = ' '.join(args.query).strip()
    if not query and not (args.type or args.priority):
        print('usage: query_bge.py [--type TYPE] [--priority PRIORITY] "查询内容"')
        print('       query_bge.py --type palace --priority high "查询内容"')
        return

    # ---- 加载数据 ----
    rows = []
    by_path = {}

    for line in INDEX.read_text(encoding='utf-8').splitlines():
        if not line.strip():
            continue
        row = json.loads(line)
        # 冷区盲化
        if is_cold_zone_path(row['path']):
            continue
        # 元数据预过滤
        if args.type and row.get('type') != args.type:
            continue
        if args.priority and row.get('priority') != args.priority:
            continue
        rows.append(row)
        by_path[row['path']] = row

    if not rows:
        print('[WATCHDOG_EMPTY] 过滤后无匹配文件')
        return

    # ---- 查询编码 ----
    if query:
        model = BGEM3FlagModel('BAAI/bge-m3', use_fp16=False)
        qv = model.encode([query], batch_size=1, max_length=128)['dense_vecs'][0]
        if hasattr(qv, 'tolist'):
            qv = qv.tolist()
    else:
        qv = None

    # ---- 评分 ----
    for row in rows:
        if qv is not None:
            cos = cosine(qv, row['embedding'])
        else:
            cos = 0.5  # 无查询时，所有文件同权

        if args.raw:
            row['score'] = max(0.0, cos)
        else:
            row['score'] = compound_score(
                semantic=cos,
                recency=get_recency(row.get('mtime', 0)),
                importance=get_importance(row.get('priority')),
            )
        row['_cosine'] = cos
        row['_recency'] = get_recency(row.get('mtime', 0))
        row['_importance'] = get_importance(row.get('priority'))

    rows.sort(key=lambda x: x['score'], reverse=True)
    top = rows[:args.top_k]

    # ---- 输出 ----
    graph = json.loads(GRAPH.read_text(encoding='utf-8')) if GRAPH.exists() else {}
    printed = set()

    for r in top:
        printed.add(r['path'])
        if args.details:
            print(json.dumps({
                'role': 'core',
                'path': r['path'],
                'type': r.get('type'),
                'priority': r.get('priority'),
                'score': round(r['score'], 6),
                'semantic': round(r['_cosine'], 4),
                'recency': round(r['_recency'], 4),
                'importance': round(r['_importance'], 4),
            }, ensure_ascii=False))
        else:
            print(json.dumps({
                'role': 'core',
                'path': r['path'],
                'type': r.get('type'),
                'priority': r.get('priority'),
                'score': round(r['score'], 6),
            }, ensure_ascii=False))

        # 1-hop 邻居
        for n in graph.get(r['path'], [])[:2]:
            if n in printed or n not in by_path:
                continue
            printed.add(n)
            nr = by_path[n]
            print(json.dumps({
                'role': 'neighbor',
                'path': n,
                'type': nr.get('type'),
                'priority': nr.get('priority'),
                'score': round(nr.get('score', 0.0), 6),
            }, ensure_ascii=False))

    if not top:
        print('[WATCHDOG_NO_MATCH]')


if __name__ == '__main__':
    main()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)