项目文件夹

文件
wehub-resource-sync 0ef5fcb1c5
Security / Dependency audit (pip-audit) (push) Has been cancelled
Security / CodeQL (javascript-typescript) (push) Has been cancelled
Security / CodeQL (python) (push) Has been cancelled
Security / Secret scan (gitleaks) (push) Has been cancelled
rust / test (ubuntu) (push) Has been cancelled
rust / simulator e2e (macos-latest) (push) Has been cancelled
rust / simulator e2e (ubuntu-latest) (push) Has been cancelled
rust / simulator e2e (windows-latest) (push) Has been cancelled
rust / wheels (aarch64-apple-darwin) (push) Has been cancelled
rust / wheels (x86_64-unknown-linux-gnu) (push) Has been cancelled
rust / wheels (x86_64-apple-darwin) (push) Has been cancelled
rust / audit (push) Has been cancelled
rust / parity (nightly, allowed to fail during Phase 0) (push) Has been cancelled
CI / commitlint (push) Has been skipped
Dev Containers / validate (.devcontainer/devcontainer.json, default) (push) Failing after 0s
Dev Containers / validate (.devcontainer/memory-stack/devcontainer.json, memory-stack) (push) Failing after 0s
Dev Containers / validate-worktree (push) Failing after 0s
CI / changes (push) Failing after 4s
Deploy Documentation / validate (push) Has been skipped
Deploy Documentation / deploy (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, claude) (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, codex) (push) Failing after 1s
Install Native E2E / install-native (ubuntu-latest) (push) Failing after 1s
OpenCode Plugin / typecheck + build + test (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, copilot) (push) Failing after 1s
Release Please / release-please (push) Failing after 1s
Wrap E2E / docker-wrap-e2e (push) Failing after 1s
Wrap Native E2E / wrap-native (ubuntu-latest) (push) Failing after 1s
Init E2E / docker-init-e2e (push) Failing after 4s
Merge Conflicts / merge-conflicts (push) Failing after 4s
CI / lint (push) Has been cancelled
CI / build-wheel (push) Has been cancelled
CI / build-wheel-windows (push) Has been cancelled
CI / prefetch-model (push) Has been cancelled
CI / test-dashboard-ui (push) Has been cancelled
CI / test (1) (push) Has been cancelled
CI / test (2) (push) Has been cancelled
CI / test (3) (push) Has been cancelled
CI / test (4) (push) Has been cancelled
CI / test-extras (push) Has been cancelled
CI / test-agno (push) Has been cancelled
CI / build (push) Has been cancelled
CI / workflow-validation (push) Has been cancelled
CI / docker-native-e2e (push) Has been cancelled
CI / windows-native-wrapper (push) Has been cancelled
CI / macos-native-wrapper (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / promote-latest (push) Has been cancelled
Init Native E2E / init-native (macos-latest, claude) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, codex) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, copilot) (push) Has been cancelled
Install Native E2E / install-native (macos-latest) (push) Has been cancelled
Wrap Native E2E / wrap-native (macos-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:03:20 +08:00

286 行
9.5 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
"""Memory file budget manager — token-optimized memory file maintenance.
Manages the token budget for agent memory files:
- Ranks memories by importance × recency × access frequency
- Prunes memories that fall below budget threshold
- Detects stale memories referencing deleted/renamed entities
- Merges similar memories to save tokens
This is Headroom's superpower applied to itself — the proxy that optimizes
LLM context also optimizes its own memory files for minimum token consumption.
"""
from __future__ import annotations
import logging
import re
import subprocess
import time
from dataclasses import dataclass, field
from pathlib import Path
from headroom._subprocess import run
from headroom.memory.writers.base import MemoryEntry, _estimate_tokens
logger = logging.getLogger(__name__)
@dataclass
class BudgetConfig:
"""Configuration for memory budget management."""
# Token budgets per agent type
agent_budgets: dict[str, int] = field(
default_factory=lambda: {
"claude": 2000, # Claude Code: 200-line MEMORY.md limit
"cursor": 3000, # Cursor: .mdc rules loaded on match
"codex": 3000, # Codex: AGENTS.md merged
"aider": 2000, # Aider: read files
"gemini": 3000, # Gemini: GEMINI.md
"generic": 3000, # Default
}
)
# Staleness detection
staleness_decay_rate: float = 0.1 # Importance decays ~10% per day
staleness_min_importance: float = 0.15 # Below this = prunable
staleness_check_git: bool = True # Check git for deleted files
# Deduplication
similarity_merge_threshold: float = 0.85 # Cosine sim above = merge
@dataclass
class BudgetReport:
"""Report from a budget optimization pass."""
total_memories: int = 0
kept: int = 0
pruned_staleness: int = 0
pruned_budget: int = 0
merged: int = 0
tokens_before: int = 0
tokens_after: int = 0
@property
def tokens_saved(self) -> int:
return self.tokens_before - self.tokens_after
class MemoryBudgetManager:
"""Manages token budgets for agent memory files.
Responsible for deciding which memories to keep, prune, or merge
to stay within token budgets while maximizing information density.
"""
def __init__(
self,
project_path: Path | None = None,
config: BudgetConfig | None = None,
) -> None:
self._project_path = project_path or Path.cwd()
self._config = config or BudgetConfig()
self._git_files_cache: set[str] | None = None
def optimize(
self,
memories: list[MemoryEntry],
agent_type: str = "generic",
) -> tuple[list[MemoryEntry], BudgetReport]:
"""Optimize a set of memories for an agent's token budget.
Returns the optimized list and a report of what changed.
Args:
memories: All candidate memories.
agent_type: Target agent type (determines budget).
Returns:
Tuple of (optimized memories, report).
"""
report = BudgetReport(total_memories=len(memories))
report.tokens_before = sum(_estimate_tokens(m.content) for m in memories)
budget = self._config.agent_budgets.get(agent_type, 3000)
# Step 1: Apply temporal decay to importance scores
decayed = self._apply_decay(memories)
# Step 2: Detect and flag stale memories
fresh, stale = self._detect_staleness(decayed)
report.pruned_staleness = len(stale)
# Step 3: Merge similar memories
merged = self._merge_similar(fresh)
report.merged = len(fresh) - len(merged)
# Step 4: Rank by score and apply budget
ranked = sorted(merged, key=lambda m: m.score, reverse=True)
budgeted: list[MemoryEntry] = []
tokens_used = 0
for m in ranked:
entry_tokens = _estimate_tokens(m.content) + 10
if tokens_used + entry_tokens > budget:
report.pruned_budget += 1
continue
tokens_used += entry_tokens
budgeted.append(m)
report.kept = len(budgeted)
report.tokens_after = tokens_used
return budgeted, report
def _apply_decay(self, memories: list[MemoryEntry]) -> list[MemoryEntry]:
"""Apply temporal decay to importance scores."""
now = time.time()
rate = self._config.staleness_decay_rate
min_importance = self._config.staleness_min_importance
result: list[MemoryEntry] = []
for m in memories:
age_days = (now - m.created_at) / 86400
# Exponential decay: importance × e^(-rate × age)
import math
decayed_importance = m.importance * math.exp(-rate * age_days)
# Access count counteracts decay
if m.access_count > 0:
access_boost = min(0.3, m.access_count * 0.05)
decayed_importance = min(1.0, decayed_importance + access_boost)
if decayed_importance >= min_importance:
m.importance = decayed_importance
result.append(m)
return result
def _detect_staleness(
self,
memories: list[MemoryEntry],
) -> tuple[list[MemoryEntry], list[MemoryEntry]]:
"""Separate fresh memories from stale ones.
A memory is stale if it references files/paths that no longer exist.
Uses git ls-files when available, falls back to filesystem checks.
"""
git_files = self._get_git_files() if self._config.staleness_check_git else set()
fresh: list[MemoryEntry] = []
stale: list[MemoryEntry] = []
for m in memories:
if self._is_stale(m, git_files):
stale.append(m)
else:
fresh.append(m)
return fresh, stale
def _is_stale(self, memory: MemoryEntry, git_files: set[str]) -> bool:
"""Check if a memory references entities that no longer exist."""
# Check entity_refs for file paths
for ref in memory.entity_refs:
if ref.startswith("/") or ref.startswith("./"):
# Absolute or relative path — check if file exists
if ref.startswith("./"):
ref = ref[2:]
# Normalize to relative
try:
rel = str(Path(ref).relative_to(self._project_path))
except ValueError:
rel = ref
if rel not in git_files and not Path(ref).exists():
return True
# Check content for backtick-quoted file paths
path_refs = re.findall(r"`([/\w.]+(?:/[\w.]+)+)`", memory.content)
for path_ref in path_refs:
# Only flag if it looks like a full path and is missing
if "/" in path_ref:
try:
rel = str(Path(path_ref).relative_to(self._project_path))
except ValueError:
rel = path_ref
if rel not in git_files and not Path(path_ref).exists():
return True
return False
def _get_git_files(self) -> set[str]:
"""Get set of tracked files in the git repo."""
if self._git_files_cache is not None:
return self._git_files_cache
try:
result = run(
["git", "ls-files"],
capture_output=True,
text=True,
cwd=self._project_path,
timeout=5,
)
if result.returncode == 0:
self._git_files_cache = set(result.stdout.strip().split("\n"))
else:
self._git_files_cache = set()
except (subprocess.TimeoutExpired, FileNotFoundError):
self._git_files_cache = set()
return self._git_files_cache
def _merge_similar(self, memories: list[MemoryEntry]) -> list[MemoryEntry]:
"""Merge memories with very similar content.
Uses simple text overlap heuristic (no embeddings required).
"""
if len(memories) <= 1:
return memories
merged: list[MemoryEntry] = []
used: set[int] = set()
for i, m1 in enumerate(memories):
if i in used:
continue
# Find similar memories
group = [m1]
for j, m2 in enumerate(memories[i + 1 :], start=i + 1):
if j in used:
continue
if (
self._text_similarity(m1.content, m2.content)
> self._config.similarity_merge_threshold
):
group.append(m2)
used.add(j)
if len(group) == 1:
merged.append(m1)
else:
# Merge: keep highest-importance content, combine entity refs
best = max(group, key=lambda m: m.importance)
all_entities = set()
for m in group:
all_entities.update(m.entity_refs)
best.entity_refs = list(all_entities)
best.access_count = max(m.access_count for m in group)
merged.append(best)
return merged
@staticmethod
def _text_similarity(a: str, b: str) -> float:
"""Simple Jaccard similarity on word sets."""
words_a = set(a.lower().split())
words_b = set(b.lower().split())
if not words_a or not words_b:
return 0.0
intersection = words_a & words_b
union = words_a | words_b
return len(intersection) / len(union)