重构仓库目录为统一的 runtime 分层并清理历史 openclaw 残留。

本次迁移将网关/通道/工具/技能/脚本与协议资源集中到新结构,统一路径常量与脚本转发机制,减少顶层噪音并保证运行与测试行为一致。

Made-with: Cursor
This commit is contained in:
oliver 2026-04-25 01:24:23 +08:00
parent ba3836f00f
commit 4a23b715a2
498 changed files with 2760 additions and 2200 deletions

View file

@ -0,0 +1,13 @@
---
name: boot-md
description: "Run BOOT.md on gateway startup"
metadata:
oclaw:
emoji: "🚀"
events: ["gateway:startup"]
---
# Boot Checklist Hook (Python)
On `gateway:startup`, looks for `BOOT.md` under common workspace roots and records a run log.

View file

@ -0,0 +1,77 @@
from __future__ import annotations
import datetime as _dt
import os
from pathlib import Path
from typing import Any, Iterable
def _resolve_state_dir() -> Path:
override = os.environ.get("OCLAW_STATE_DIR") or os.environ.get("OCLAW_HOME")
if override and override.strip():
return Path(os.path.expanduser(override.strip())).resolve()
return Path.home() / ".oclaw"
def _candidate_roots(event: Any) -> list[Path]:
roots: list[Path] = []
ctx = getattr(event, "context", {}) or {}
# 1) explicit workspaceDir in hook event
ws = ctx.get("workspaceDir") if isinstance(ctx, dict) else None
if isinstance(ws, str) and ws.strip():
roots.append(Path(ws).expanduser())
# 2) OCLAW_WORKSPACE env
env_ws = str(os.getenv("OCLAW_WORKSPACE") or "").strip()
if env_ws:
roots.append(Path(env_ws).expanduser())
# 3) repo-local conventional roots
# handler.py is under oclaw/hooks/bundled/boot-md/
repo = Path(__file__).resolve().parents[4]
roots.extend(
[
repo / "oclaw" / "runtime" / "assets" / "agent_workspaces" / "workspace-main",
repo / "oclaw" / "workspace-main",
repo / "oclaw" / "workspace",
repo,
]
)
# de-dupe
out: list[Path] = []
seen: set[str] = set()
for p in roots:
key = str(p.resolve()) if p.exists() else str(p)
if key in seen:
continue
seen.add(key)
out.append(p)
return out
def handle(event: Any) -> None:
if getattr(event, "type", None) != "gateway" or getattr(event, "action", None) != "startup":
return
state = _resolve_state_dir()
log_dir = state / "logs"
log_dir.mkdir(parents=True, exist_ok=True)
out_log = log_dir / "boot-md.log"
now = getattr(event, "timestamp", None)
if not isinstance(now, _dt.datetime):
now = _dt.datetime.now(tz=_dt.timezone.utc)
roots = _candidate_roots(event)
checked = 0
found = 0
lines: list[str] = []
for root in roots:
checked += 1
boot = root / "BOOT.md"
if boot.exists() and boot.is_file():
found += 1
lines.append(f"[{now.isoformat()}] FOUND {boot}")
else:
lines.append(f"[{now.isoformat()}] MISS {boot}")
out_log.write_text("\n".join(lines) + "\n", encoding="utf-8")

View file

@ -0,0 +1,30 @@
---
name: bootstrap-extra-files
description: "Inject additional workspace bootstrap files via glob/path patterns"
metadata:
oclaw:
emoji: "📎"
events: ["agent:bootstrap"]
---
# Bootstrap Extra Files Hook (Python)
On `agent:bootstrap`, expands extra file glob patterns and appends them to `event.context.bootstrapFiles`.
Config example (hook key `bootstrap-extra-files`):
```json
{
"hooks": {
"internal": {
"entries": {
"bootstrap-extra-files": {
"enabled": true,
"paths": ["packages/*/AGENTS.md", "packages/*/TOOLS.md"]
}
}
}
}
}
```

View file

@ -0,0 +1,146 @@
from __future__ import annotations
import fnmatch
from pathlib import Path
from typing import Any, Dict, Iterable, List, Sequence
HOOK_KEY = "bootstrap-extra-files"
_ALLOWED_BASENAMES = {
"AGENTS.md",
"SOUL.md",
"TOOLS.md",
"IDENTITY.md",
"USER.md",
"HEARTBEAT.md",
"BOOTSTRAP.md",
"MEMORY.md",
"memory.md",
}
def _resolve_hook_cfg(cfg: Any) -> Dict[str, Any]:
if not isinstance(cfg, dict):
return {}
hooks = cfg.get("hooks") if isinstance(cfg.get("hooks"), dict) else {}
internal = hooks.get("internal") if isinstance(hooks.get("internal"), dict) else {}
entries = internal.get("entries") if isinstance(internal.get("entries"), dict) else {}
row = entries.get(HOOK_KEY)
return row if isinstance(row, dict) else {}
def _string_list(v: Any) -> List[str]:
if isinstance(v, list):
out = []
for x in v:
s = str(x or "").strip()
if s:
out.append(s)
return out
if isinstance(v, str):
s = v.strip()
return [s] if s else []
return []
def _patterns(hook_cfg: Dict[str, Any]) -> List[str]:
for k in ("paths", "patterns", "files"):
got = _string_list(hook_cfg.get(k))
if got:
return got
return []
def _is_within(root: Path, candidate: Path) -> bool:
try:
candidate.resolve().relative_to(root.resolve())
return True
except Exception:
return False
def _glob_paths(root: Path, patterns: Sequence[str]) -> List[Path]:
"""
We intentionally do not use Path.glob on arbitrary patterns that might escape roots via '..'.
Instead: enumerate candidates by rglob and fnmatch on posix-style relative paths.
"""
if not root.exists() or not root.is_dir():
return []
# Pre-normalize patterns to forward-slash for fnmatch
raw_pats = [p.replace("\\", "/").lstrip("/") for p in patterns if str(p or "").strip()]
pats: list[str] = []
for pat in raw_pats:
pats.append(pat)
# Python's fnmatch doesn't treat "**/" as "zero-or-more directories".
# Add a compatibility variant so "**/AGENTS.md" matches "AGENTS.md" as well.
if pat.startswith("**/") and len(pat) > 3:
pats.append(pat[3:])
if not pats:
return []
out: List[Path] = []
try:
for p in root.rglob("*"):
if not p.is_file():
continue
if p.name not in _ALLOWED_BASENAMES:
continue
rel = p.relative_to(root).as_posix()
if any(fnmatch.fnmatch(rel, pat) for pat in pats):
out.append(p)
except Exception:
return out
# deterministic order
out.sort(key=lambda x: x.as_posix())
return out
def handle(event: Any) -> None:
if getattr(event, "type", None) != "agent" or getattr(event, "action", None) != "bootstrap":
return
ctx = getattr(event, "context", None)
if not isinstance(ctx, dict):
return
hook_cfg = _resolve_hook_cfg(ctx.get("cfg"))
if hook_cfg.get("enabled") is False:
return
patterns = _patterns(hook_cfg)
if not patterns:
return
ws = ctx.get("workspaceDir")
if not isinstance(ws, str) or not ws.strip():
return
ws_root = Path(ws).expanduser()
if not ws_root.exists() or not ws_root.is_dir():
return
matches = _glob_paths(ws_root, patterns)
if not matches:
return
# Mutate context.bootstrapFiles (Oclaw-style).
boot = ctx.get("bootstrapFiles")
if not isinstance(boot, list):
boot = []
ctx["bootstrapFiles"] = boot
existing_paths = set()
for it in list(boot):
if isinstance(it, dict) and isinstance(it.get("path"), str):
existing_paths.add(it["path"])
elif isinstance(it, str):
existing_paths.add(it)
for p in matches:
ap = str(p.resolve())
if ap in existing_paths:
continue
boot.append({"path": ap, "name": p.name})
existing_paths.add(ap)

View file

@ -0,0 +1,13 @@
---
name: command-logger
description: "Log all command events to a centralized audit file"
metadata:
oclaw:
emoji: "📝"
events: ["command"]
---
# Command Logger Hook (Python)
Logs all `command` events to `~/.oclaw/logs/commands.log` (JSONL).

View file

@ -0,0 +1,35 @@
from __future__ import annotations
import json
import os
from pathlib import Path
from typing import Any, Dict
def _resolve_state_dir() -> Path:
# Keep this compatible with typical Oclaw layouts.
override = os.environ.get("OCLAW_STATE_DIR") or os.environ.get("OCLAW_HOME")
if override and override.strip():
return Path(os.path.expanduser(override.strip())).resolve()
return Path.home() / ".oclaw"
def handle(event) -> None:
if getattr(event, "type", None) != "command":
return
state_dir = _resolve_state_dir()
log_dir = state_dir / "logs"
log_dir.mkdir(parents=True, exist_ok=True)
payload: Dict[str, Any] = {
"timestamp": getattr(getattr(event, "timestamp", None), "isoformat", lambda: None)(),
"action": getattr(event, "action", None),
"sessionKey": getattr(event, "sessionKey", None),
"senderId": (getattr(event, "context", {}) or {}).get("senderId", "unknown"),
"source": (getattr(event, "context", {}) or {}).get("commandSource", "unknown"),
}
with (log_dir / "commands.log").open("a", encoding="utf-8") as f:
f.write(json.dumps(payload, ensure_ascii=False) + "\n")

View file

@ -0,0 +1,17 @@
---
name: session-memory
description: "Save session context to memory when /new or /reset command is issued"
metadata:
oclaw:
emoji: "💾"
events: ["command:new", "command:reset"]
---
# Session Memory Hook (Python)
On `command:new` / `command:reset`, exports the latest N messages of the session into
`<workspace>/memory/YYYY-MM-DD-<slug>.md`.
Notes:
- This Python port reads messages from SQLite (`SqliteStore`) instead of workspace session transcript files.

View file

@ -0,0 +1,165 @@
from __future__ import annotations
import datetime as _dt
import os
import re
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
HOOK_KEY = "session-memory"
def _ensure_repo_imports() -> None:
# Allow importing repository modules when hook is loaded by path.
# handler.py is under oclaw/hooks/bundled/session-memory/
repo = Path(__file__).resolve().parents[5]
if str(repo) not in sys.path:
sys.path.insert(0, str(repo))
def _resolve_hook_cfg(cfg: Any) -> Dict[str, Any]:
if not isinstance(cfg, dict):
return {}
hooks = cfg.get("hooks") if isinstance(cfg.get("hooks"), dict) else {}
internal = hooks.get("internal") if isinstance(hooks.get("internal"), dict) else {}
entries = internal.get("entries") if isinstance(internal.get("entries"), dict) else {}
row = entries.get(HOOK_KEY)
return row if isinstance(row, dict) else {}
_SLUG_SAFE_RE = re.compile(r"[^a-z0-9]+")
def _slugify(text: str, *, max_len: int = 40) -> str:
s = (text or "").strip().lower()
s = _SLUG_SAFE_RE.sub("-", s).strip("-")
if not s:
return ""
s = s[:max_len].strip("-")
return s or ""
def _fallback_time_slug(ts: _dt.datetime) -> str:
return ts.strftime("%H%M")
def _workspace_dir_from_event(event: Any) -> Optional[Path]:
ctx = getattr(event, "context", None)
if not isinstance(ctx, dict):
return None
ws = ctx.get("workspaceDir")
if isinstance(ws, str) and ws.strip():
return Path(ws).expanduser()
env_ws = str(os.getenv("OCLAW_WORKSPACE") or "").strip()
if env_ws:
return Path(env_ws).expanduser()
return None
def _recent_conversation_lines(msgs: List[Any], *, max_pairs: int) -> str:
"""
Render a minimal markdown "conversation" block.
"""
# Keep only user/assistant/tool-ish messages; show role prefixes.
lines: list[str] = []
for m in msgs[-max(1, max_pairs * 2) :]:
role = str(getattr(m, "role", "") or "").strip() or "unknown"
content = str(getattr(m, "content", "") or "").strip()
if not content:
continue
if role.lower() == "assistant":
prefix = "Assistant"
elif role.lower() == "user":
prefix = "User"
else:
prefix = role
lines.append(f"- **{prefix}**: {content}")
return "\n".join(lines).strip()
def handle(event: Any) -> None:
# Only trigger on command new/reset
if getattr(event, "type", None) != "command":
return
action = str(getattr(event, "action", "") or "").strip().lower()
if action not in {"new", "reset"}:
return
ctx = getattr(event, "context", None)
if not isinstance(ctx, dict):
ctx = {}
cfg = ctx.get("cfg")
hook_cfg = _resolve_hook_cfg(cfg)
if hook_cfg.get("enabled") is False:
return
max_msgs = hook_cfg.get("messages")
try:
max_msgs_n = int(max_msgs) if max_msgs is not None else 15
except Exception:
max_msgs_n = 15
max_msgs_n = max(5, min(max_msgs_n, 200))
ts = getattr(event, "timestamp", None)
if not isinstance(ts, _dt.datetime):
ts = _dt.datetime.now(tz=_dt.timezone.utc)
if ts.tzinfo is None:
ts = ts.replace(tzinfo=_dt.timezone.utc)
ws_dir = _workspace_dir_from_event(event)
if ws_dir is None:
# Nothing to do without a workspace dir target.
return
ws_dir.mkdir(parents=True, exist_ok=True)
mem_dir = ws_dir / "memory"
mem_dir.mkdir(parents=True, exist_ok=True)
session_id = str(getattr(event, "sessionKey", "") or "").strip() or "unknown"
# Fetch recent messages from sqlite.
try:
_ensure_repo_imports()
from oclaw.platform.config.paths import db_path # type: ignore
from oclaw.platform.persistence.sqlite_store import SqliteStore # type: ignore
store = SqliteStore(db_path())
msgs = store.get_messages(session_id=session_id, limit=max_msgs_n)
except Exception:
msgs = []
convo = _recent_conversation_lines(list(msgs or []), max_pairs=max_msgs_n)
# Build slug from first user message in window.
base_slug = ""
for m in list(msgs or []):
if str(getattr(m, "role", "") or "").strip().lower() != "user":
continue
t = str(getattr(m, "content", "") or "").strip()
if t:
base_slug = _slugify(t)
break
if not base_slug:
base_slug = _fallback_time_slug(ts)
date_str = ts.date().isoformat()
filename = f"{date_str}-{base_slug}.md"
target = mem_dir / filename
header = [
f"# Session: {date_str} {ts.strftime('%H:%M:%S')} UTC",
"",
f"- **Session Key**: {session_id}",
f"- **Action**: {action}",
"",
]
body: list[str] = []
if convo:
body.extend(["## Conversation Summary", "", convo, ""])
else:
body.extend(["## Conversation Summary", "", "- (no messages found)", ""])
target.write_text("\n".join(header + body).strip() + "\n", encoding="utf-8")

View file

@ -0,0 +1,13 @@
---
name: wiki-auto-inject
description: "Inject wiki context before model prompt build"
metadata:
oclaw:
emoji: "📚"
events: ["llm:before_prompt_build"]
---
# Wiki Auto Inject Hook
Builds a compact wiki context block and prepends it to system prompt via
`event.context.prepend_system_context`.

View file

@ -0,0 +1,345 @@
from __future__ import annotations
import json
import re
from pathlib import Path
from typing import Any
def _project_root() -> Path:
return Path(__file__).resolve().parents[4]
def _load_config() -> dict[str, Any]:
cfg_path = _project_root() / "oclaw" / "oclaw.json"
if not cfg_path.exists():
return {}
try:
data = json.loads(cfg_path.read_text(encoding="utf-8"))
return data if isinstance(data, dict) else {}
except Exception:
return {}
def _resolve_wiki_entry(cfg: dict[str, Any]) -> dict[str, Any]:
plugins = cfg.get("plugins") if isinstance(cfg, dict) else {}
entries = plugins.get("entries") if isinstance(plugins, dict) else {}
entry = entries.get("memory-wiki") if isinstance(entries, dict) else {}
return entry if isinstance(entry, dict) else {}
def _resolve_runtime(entry: dict[str, Any]) -> tuple[Path, int, int, bool, int, bool]:
root_cfg = str(entry.get("wiki_root") or "oclaw/docs/memory-system/wiki").strip()
root = Path(root_cfg)
if not root.is_absolute():
root = (_project_root() / root).resolve()
auto = entry.get("auto") if isinstance(entry.get("auto"), dict) else {}
inject = auto.get("inject") if isinstance(auto, dict) else {}
max_chars = int(inject.get("max_chars") or 1800)
top_k = int(inject.get("top_k") or 6)
ultra_saver_enabled = bool(inject.get("ultra_saver_enabled", False))
min_query_chars = int(inject.get("min_query_chars") or 20)
require_topic_hint = bool(inject.get("require_topic_hint", True))
return (
root,
max(500, min(max_chars, 8000)),
max(1, min(top_k, 20)),
ultra_saver_enabled,
max(1, min(min_query_chars, 500)),
require_topic_hint,
)
def _enabled(entry: dict[str, Any]) -> bool:
auto = entry.get("auto") if isinstance(entry.get("auto"), dict) else {}
return bool(auto.get("enabled", False))
def _default_topic_rules() -> list[dict[str, Any]]:
return [
{"topic": "network", "keywords": ["vlan", "router", "switch", "network", "dns", "gateway"]},
{"topic": "devops", "keywords": ["deploy", "k8s", "kubernetes", "docker", "ci", "ops"]},
{"topic": "engineering", "keywords": ["bug", "fix", "todo", "feature", "refactor", "test"]},
]
def _resolve_topic_rules(entry: dict[str, Any]) -> list[dict[str, Any]]:
auto = entry.get("auto") if isinstance(entry.get("auto"), dict) else {}
routing = auto.get("topic_routing") if isinstance(auto.get("topic_routing"), dict) else {}
rules = routing.get("rules")
if not isinstance(rules, list):
return _default_topic_rules()
out: list[dict[str, Any]] = []
for rule in rules:
if not isinstance(rule, dict):
continue
topic = str(rule.get("topic") or "").strip().lower()
kws = rule.get("keywords")
if not topic or not isinstance(kws, list):
continue
keywords = [str(k).strip().lower() for k in kws if str(k).strip()]
if not keywords:
continue
out.append({"topic": topic, "keywords": keywords})
return out or _default_topic_rules()
def _query_terms(query: str) -> list[str]:
return [t for t in re.split(r"\s+", query.strip().lower()) if len(t) >= 2]
def _score_line(*, query: str, terms: list[str], text: str) -> float:
low = text.lower()
if not low:
return 0.0
score = 0.0
if query and query in low:
score += 4.0
hit_terms = 0
for term in terms:
if term in low:
hit_terms += 1
score += 1.2
if hit_terms > 1:
score += 0.8
return score
def _line_snippet(lines: list[str], idx: int) -> str:
parts: list[str] = []
start = max(0, idx - 1)
end = min(len(lines), idx + 2)
for i in range(start, end):
line = str(lines[i] or "").strip()
if not line:
continue
parts.append(line)
return " | ".join(parts)
def _load_index_file_set(wiki_root: Path) -> set[str]:
idx = wiki_root / ".oclaw" / "index.json"
if not idx.exists():
return set()
try:
obj = json.loads(idx.read_text(encoding="utf-8"))
except Exception:
return set()
files = obj.get("files") if isinstance(obj, dict) else None
if not isinstance(files, list):
return set()
out: set[str] = set()
for one in files:
rel = str(one or "").replace("\\", "/").strip().lstrip("/")
if rel:
out.add(rel)
return out
def _load_topic_index(wiki_root: Path) -> dict[str, str]:
p = wiki_root / ".oclaw" / "topic-index.json"
if not p.exists():
return {}
try:
obj = json.loads(p.read_text(encoding="utf-8"))
except Exception:
return {}
topics = obj.get("topics") if isinstance(obj, dict) else None
if not isinstance(topics, dict):
return {}
out: dict[str, str] = {}
for topic, meta in topics.items():
if not isinstance(topic, str) or not isinstance(meta, dict):
continue
rel = str(meta.get("path") or "").replace("\\", "/").strip().lstrip("/")
if rel:
out[topic.lower()] = rel
return out
def _query_topic_hints(query: str, rules: list[dict[str, Any]]) -> list[str]:
low = str(query or "").lower()
hints: list[str] = []
for rule in rules:
topic = str(rule.get("topic") or "").strip().lower()
kws = rule.get("keywords") if isinstance(rule.get("keywords"), list) else []
if not topic:
continue
if any(str(k).lower() in low for k in kws):
hints.append(topic)
return hints
def _candidate_files(wiki_root: Path) -> list[Path]:
preferred: list[Path] = []
merged = wiki_root / "inbox" / "merged-turns.md"
if merged.exists() and merged.is_file():
preferred.append(merged)
index_set = _load_index_file_set(wiki_root)
files = sorted([p for p in wiki_root.rglob("*.md") if p.is_file()])
if not files:
return preferred
if not index_set:
return preferred + [p for p in files if p not in preferred]
prioritized = []
fallback = []
for p in files:
if p in preferred:
continue
rel = str(p.relative_to(wiki_root)).replace("\\", "/")
if rel in index_set:
prioritized.append(p)
else:
fallback.append(p)
return preferred + prioritized + fallback
def _candidate_files_for_query(wiki_root: Path, query: str, topic_rules: list[dict[str, Any]]) -> list[Path]:
preferred: list[Path] = []
topic_map = _load_topic_index(wiki_root)
for hint in _query_topic_hints(query, topic_rules):
rel = topic_map.get(hint)
if not rel:
continue
p = wiki_root / rel
if p.exists() and p.is_file() and p not in preferred:
preferred.append(p)
merged = wiki_root / "inbox" / "merged-turns.md"
if merged.exists() and merged.is_file() and merged not in preferred:
preferred.append(merged)
rest = _candidate_files(wiki_root)
return preferred + [p for p in rest if p not in preferred]
def _collect_snippets(
wiki_root: Path,
query: str,
max_chars: int,
top_k: int,
topic_rules: list[dict[str, Any]] | None = None,
) -> tuple[str, list[dict[str, Any]]]:
if not wiki_root.exists():
return "", []
q = query.strip().lower()
if not q:
return "", []
terms = _query_terms(query)
if not terms and len(query) < 2:
return "", []
rules = topic_rules or _default_topic_rules()
candidates: list[tuple[float, int, str, int, str]] = []
for source_rank, fp in enumerate(_candidate_files_for_query(wiki_root, query, rules)):
rel = str(fp.relative_to(wiki_root)).replace("\\", "/")
try:
lines = fp.read_text(encoding="utf-8").splitlines()
except Exception:
continue
for idx, line in enumerate(lines, start=1):
txt = line.strip()
if not txt:
continue
score = _score_line(query=q, terms=terms, text=txt)
if score <= 0:
continue
snippet = _line_snippet(lines, idx - 1)
one = f"- {rel}:{idx} {snippet}"
candidates.append((score, int(source_rank), rel, idx, one))
if not candidates:
return "", []
candidates.sort(key=lambda x: (-x[0], x[1], x[2], x[3]))
blocks: list[str] = []
meta: list[dict[str, Any]] = []
total = 0
truncated = False
for score, _rank, _rel, _idx, one in candidates[: top_k * 10]:
if any(one == exist for exist in blocks):
continue
if total + len(one) + 1 > max_chars:
truncated = True
break
blocks.append(one)
meta.append({"source": _rel, "line": int(_idx), "score": round(float(score), 3)})
total += len(one) + 1
if len(blocks) >= top_k:
truncated = len(candidates) > len(meta)
break
if truncated:
for item in meta:
item["truncated"] = True
return "\n".join(blocks).strip(), meta
def handle(event: Any) -> None:
if getattr(event, "type", None) != "llm" or getattr(event, "action", None) != "before_prompt_build":
return
ctx = getattr(event, "context", None)
if not isinstance(ctx, dict):
return
memory_mode = str(ctx.get("memory_mode") or "default").strip().lower()
if memory_mode == "store_only":
ctx["wiki_inject_meta"] = {
"enabled": False,
"memory_mode": "store_only",
"skip_reason": "memory_mode_store_only",
}
return
cfg = _load_config()
entry = _resolve_wiki_entry(cfg)
if not _enabled(entry):
ctx["wiki_inject_meta"] = {
"enabled": False,
"memory_mode": memory_mode,
"skip_reason": "auto_disabled",
}
return
wiki_root, max_chars, top_k, ultra_saver_enabled, min_query_chars, require_topic_hint = _resolve_runtime(entry)
topic_rules = _resolve_topic_rules(entry)
query = str(ctx.get("userText") or "").strip()
if ultra_saver_enabled and len(query) < min_query_chars:
ctx["wiki_inject_meta"] = {
"enabled": False,
"memory_mode": memory_mode,
"ultra_saver_enabled": True,
"skip_reason": "short_query",
"min_query_chars": int(min_query_chars),
"query_len": int(len(query)),
}
return
if ultra_saver_enabled and require_topic_hint and not _query_topic_hints(query, topic_rules):
ctx["wiki_inject_meta"] = {
"enabled": False,
"memory_mode": memory_mode,
"ultra_saver_enabled": True,
"skip_reason": "no_topic_hint",
}
return
snippets, inject_meta = _collect_snippets(
wiki_root,
query,
max_chars=max_chars,
top_k=top_k,
topic_rules=topic_rules,
)
if not snippets:
ctx["wiki_inject_meta"] = {
"enabled": False,
"memory_mode": memory_mode,
"top_k": int(top_k),
"max_chars": int(max_chars),
"ultra_saver_enabled": bool(ultra_saver_enabled),
"skip_reason": "no_snippets",
}
return
ctx["wiki_inject_meta"] = {
"enabled": True,
"memory_mode": memory_mode,
"top_k": int(top_k),
"max_chars": int(max_chars),
"ultra_saver_enabled": bool(ultra_saver_enabled),
"hits": inject_meta,
}
ctx["prepend_system_context"] = (
"## Wiki Context (Auto Inject)\n"
"Use as background context only; prefer latest user request when conflicts exist.\n\n"
f"{snippets}"
)