#!/usr/bin/env bash # session-compact.sh — 会话压缩与快速存档 + Write Gate 前置检查 # # v3 — 加入 pre-compact 钩子:上下文压缩前自动扫描未保存的纠正/决策/承诺 # v2 — 加入 Write Gate 自动路由:preference/correction/decision 触发寄存器写入 # # 用法: # bash scripts/session-compact.sh save "任何想记住的内容" # bash scripts/session-compact.sh save --type preference "偏好内容" # bash scripts/session-compact.sh save --type correction "纠正内容" # bash scripts/session-compact.sh save --type decision "决策内容" # bash scripts/session-compact.sh pre-compact # 上下文压缩前扫描 + 同步 # bash scripts/session-compact.sh status # 看会话统计 # bash scripts/session-compact.sh flush # 触发 Dream Cycle 同步 # bash scripts/session-compact.sh check # Write Gate 诊断 set -euo pipefail WORKSPACE="$(cd "$(dirname "$0")/.." && pwd)" MEMORY_SERVER="http://111.229.145.18" # 加载环境变量(MEMORY_API_KEY 等) if [ -z "${MEMORY_API_KEY:-}" ] && [ -f ~/.zshrc ]; then MEMORY_API_KEY=$(grep '^export MEMORY_API_KEY=' ~/.zshrc 2>/dev/null | cut -d'"' -f2 || true) fi export MEMORY_API_KEY DAILY="$WORKSPACE/memory/daily/$(date +%Y-%m-%d).md" REGISTERS="$WORKSPACE/memory/registers" SESSION_LOG="$WORKSPACE/memory/.session-compact.log" # ── Write Gate 路由 ────────────────────────────────────── write_to_register() { local type="$1" content="$2" timestamp timestamp=$(date '+%Y-%m-%d %H:%M') case "$type" in preference) printf "\n| %s | %s | %s | session-compact | medium |\n" \ "$(wc -l < "$REGISTERS/preferences.md" | tr -d ' ')" \ "$content" "$timestamp" >> "$REGISTERS/preferences.md" echo " 📝 → preferences 寄存器" ;; correction) printf "\n[correction: %s]\n- **new**: %s\n- **source**: session-compact\n" \ "$timestamp" "$content" >> "$REGISTERS/open-loops.md" echo " 🔧 → open-loops 寄存器 (correction)" ;; decision) printf "\n- %s: %s (session-compact)\n" "$timestamp" "$content" \ >> "$WORKSPACE/memory/vault.md" echo " 📋 → vault.md (decision)" ;; event) # 事件记录到 daily/ 已足够,不写寄存器 echo " 📌 → daily/ (event,不写寄存器)" ;; fact|behavior|code|rule|tool|project) # 新类型:写入 vault.md 的决策时间线 printf "\n- %s: [%s] %s (session-compact)\n" "$timestamp" "$type" "$content" \ >> "$WORKSPACE/memory/vault.md" echo " 📋 → vault.md ($type)" ;; *) echo " ⚠️ 未知类型 '$type',仅写入 daily/" ;; esac } # ── 命令处理 ───────────────────────────────────────────── case "${1:-help}" in save) shift # 解析参数 type="" content="" recall_context="" while [ $# -gt 0 ]; do case "$1" in --type) type="$2"; shift 2 ;; --type=*) type="${1#*=}"; shift ;; --recall-context) recall_context="$2"; shift 2 ;; --recall-context=*) recall_context="${1#*=}"; shift ;; *) content="$1"; shift ;; esac done if [ -z "$content" ]; then echo "用法: bash scripts/session-compact.sh save [--type ] [--recall-context ] <内容>" exit 1 fi # ── Step 1: LLM Write Gate 判断(如果未指定 type)── if [ -z "$type" ] && [ -f "$WORKSPACE/scripts/memory-write-gate.sh" ]; then echo " 🧠 Write Gate LLM 判断中..." GATE_RESULT=$(echo "$content" | bash "$WORKSPACE/scripts/memory-write-gate.sh" 2>/dev/null || echo '{"should_remember":false}') SHOULD_REMEMBER=$(echo "$GATE_RESULT" | jq -r '.should_remember // false' 2>/dev/null || echo "false") if [ "$SHOULD_REMEMBER" = "true" ]; then type=$(echo "$GATE_RESULT" | jq -r '.type // ""' 2>/dev/null || echo "") importance=$(echo "$GATE_RESULT" | jq -r '.importance // 0.5' 2>/dev/null || echo "0.5") gate_content=$(echo "$GATE_RESULT" | jq -r '.content // ""' 2>/dev/null || echo "") gate_recall=$(echo "$GATE_RESULT" | jq -r '.recall_context // [] | join(",")' 2>/dev/null || echo "") gate_tags=$(echo "$GATE_RESULT" | jq -r '.tags // [] | join(",")' 2>/dev/null || echo "") # 用 LLM 精炼的内容覆盖原始内容 [ -n "$gate_content" ] && [ "$gate_content" != "null" ] && content="$gate_content" # 用 LLM 的 recall_context(如果用户没指定) [ -z "$recall_context" ] && [ -n "$gate_recall" ] && [ "$gate_recall" != "null" ] && recall_context="$gate_recall" echo " ✅ Write Gate: 值得记 (type=$type, importance=$importance)" else gate_reason=$(echo "$GATE_RESULT" | jq -r '.reason // "not_important"' 2>/dev/null || echo "not_important") echo " ℹ️ Write Gate: 不值得记 ($gate_reason),仅写入 daily/" fi fi # ── Step 2: 写入 daily/(带 YAML frontmatter)── mkdir -p "$(dirname "$DAILY")" memory_id="mem_$(date +%Y%m%d)_$(openssl rand -hex 3 2>/dev/null || date +%s)" # 如果有 recall_context,用 YAML frontmatter 格式 if [ -n "$type" ] || [ -n "$recall_context" ]; then { echo "" echo "---" echo "memory_id: $memory_id" [ -n "$type" ] && echo "type: $type" echo "created_at: $(date -Iseconds)" [ -n "$recall_context" ] && echo "recall_context: [$recall_context]" echo "---" echo "[session-flush] $(date +%H:%M) — $content" } >> "$DAILY" else echo "" >> "$DAILY" echo "[session-flush] $(date +%H:%M) — $content" >> "$DAILY" fi echo " ✅ 已写入 $DAILY (id: $memory_id)" # ── Step 3: Write Gate — 写寄存器 ── if [ -n "$type" ]; then write_to_register "$type" "$content" fi # ── Step 4: ContextGraph 集成 ── if [ "$type" = "decision" ] || [ "$type" = "correction" ] || [ "$type" = "event" ]; then node -e " try { const { ContextGraph } = require('$WORKSPACE/src/memory/context-graph'); const g = new ContextGraph(); const entityId = 'session:$(date +%Y%m%d)'; g.entity(entityId, { type: 'session', name: 'Session $(date +%Y-%m-%d)' }); g.observe(entityId, { type: '$type', priority: '$type' === 'decision' ? 'high' : 'normal', summary: process.argv[1].substring(0, 200), details: { source: 'session-compact', memory_id: '$memory_id' } }); g.save(); console.log(' 🕸️ → ContextGraph'); } catch(e) { console.log(' ⚠️ ContextGraph: ' + e.message); } " "$content" 2>/dev/null || echo " ⚠️ ContextGraph 写入失败" fi # ── Step 5: 推到远程服务器(带新字段)── clean=$(echo "$content" | sed 's/"/\\"/g' | head -c 400) remote_payload=$(jq -n \ --arg c "$clean" \ --arg t "${type:-session-flush}" \ --arg rc "${recall_context:-}" \ --arg mid "$memory_id" \ '{content:$c, type:$t, project:"xiaolong", source:"session-compact", memory_id:$mid, recall_context:($rc | split(",") | map(select(. != "")))}') resp=$(curl -s --max-time 5 -X POST "$MEMORY_SERVER/api/v2/add" \ -H "Content-Type: application/json" \ -H "X-API-Key: ${MEMORY_API_KEY:-}" \ -d "$remote_payload" 2>/dev/null) || true if echo "$resp" | grep -q 'stored\|ok'; then echo " ✅ 已同步到远程服务器" else echo " ⚠️ 远程同步: ${resp:-无响应}" fi echo "$(date '+%Y-%m-%d %H:%M') | ${type:-general} | $content" >> "$SESSION_LOG" ;; check) echo "🔍 Write Gate 诊断" echo "" echo " 寄存器状态:" for f in "$REGISTERS"/*.md; do [ -f "$f" ] || continue name=$(basename "$f" .md) lines=$(wc -l < "$f" | tr -d ' ') size=$(wc -c < "$f" | tr -d ' ') echo " $name: ${lines}行 / ${size}字节" done echo "" echo " 最近 5 条 compact:" [ -f "$SESSION_LOG" ] && tail -5 "$SESSION_LOG" || echo " 无记录" echo "" echo " 今天 daily:" [ -f "$DAILY" ] && wc -l < "$DAILY" | xargs echo " lines:" || echo " (空)" ;; status) echo "📊 会话状态" echo " 每日日志: $DAILY" echo " 寄存器:" ls "$REGISTERS"/*.md 2>/dev/null | while read -r f; do echo " $(basename "$f" .md): $(wc -l < "$f") 行" done echo " Compact 历史:" [ -f "$SESSION_LOG" ] && tail -5 "$SESSION_LOG" || echo " 无记录" ;; flush) echo "🔄 触发立即存档..." bash "$WORKSPACE/scripts/dream-cycle.sh" sync-remote 2>&1 ;; pre-compact) echo "🛡️ Pre-Compaction Hook — 上下文压缩前安全检查" echo "" has_flush=false if [ -f "$DAILY" ]; then if grep -q '\[session-flush\]' "$DAILY"; then echo " ✅ 今天已有 [session-flush] 标记" has_flush=true grep '\[session-flush\]' "$DAILY" | tail -3 | while read -r line; do echo " $(echo "$line" | cut -c1-80)" done fi fi if [ "$has_flush" = false ]; then echo " ⚠️ 今天 daily/ 无 [session-flush] 标记 — 压缩前请先存档关键信息" fi echo "" echo " 🌐 远程服务器:" remote_resp=$(curl -s --max-time 5 "$MEMORY_SERVER/api/v2/stats" 2>/dev/null || echo '{"error":"unreachable"}') if echo "$remote_resp" | grep -q 'xiaolong'; then echo " ✅ 在线" else echo " ⚠️ 不可达" fi echo "" echo " 📤 触发同步..." bash "$WORKSPACE/scripts/dream-cycle.sh" sync-remote 2>&1 | sed 's/^/ /' echo "" echo " ✅ Pre-Compaction Hook 完成" ;; *) echo "用法:" echo " bash scripts/session-compact.sh save <内容> — 快速存关键信息" echo " bash scripts/session-compact.sh save --type preference <内容> — 记偏好(→registers)" echo " bash scripts/session-compact.sh save --type correction <内容> — 记纠正(→registers)" echo " bash scripts/session-compact.sh save --type decision <内容> — 记决策(→vault)" echo " bash scripts/session-compact.sh pre-compact — 压缩前安全检查 + 同步" echo " bash scripts/session-compact.sh status — 查看状态" echo " bash scripts/session-compact.sh check — Write Gate 诊断" echo " bash scripts/session-compact.sh flush — 触发全部同步" ;; esac