From b15ff4c31cb309093c2ea550c86db1eb68a8b006 Mon Sep 17 00:00:00 2001 From: oliver Date: Wed, 6 May 2026 23:42:52 +0800 Subject: [PATCH] fix(prompt): invalidate bootstrap cache on wiki changes Add a session-bootstrap content signature to the executor prompt cache key so identity/wiki updates invalidate cached system prompts and take effect on the next turn. Co-authored-by: Cursor --- runtime/system_prompt.py | 57 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/runtime/system_prompt.py b/runtime/system_prompt.py index 78a01240..c3e06e5f 100644 --- a/runtime/system_prompt.py +++ b/runtime/system_prompt.py @@ -1,8 +1,11 @@ from __future__ import annotations +import hashlib import threading +from pathlib import Path from typing import Any +from oclaw.platform.config.paths import PROJECT_ROOT from oclaw.runtime.memory_stage import render_memory_context_block from oclaw.runtime.project_context_prompt import build_project_context_block from oclaw.runtime.skills_prompt import build_skills_catalog_block @@ -19,6 +22,59 @@ _EXECUTOR_STATIC_PROMPT_CACHE_LOCK = threading.Lock() _EXECUTOR_STATIC_PROMPT_CACHE: dict[tuple[Any, ...], str] = {} +def _file_stat_signature(path: Path) -> str: + try: + st = path.stat() + return f"{path.as_posix()}:{int(st.st_mtime_ns)}:{int(st.st_size)}" + except Exception: + return f"{path.as_posix()}:missing" + + +def _session_bootstrap_content_signature(*, skill_binding_role: str | None, workspace_dir: str | None) -> str: + """Best-effort signature for session-bootstrap content sources. + + When this signature changes, cached executor system prompts are invalidated, + so the next turn rebuilds project context and re-runs bootstrap hooks. + """ + repo = Path(PROJECT_ROOT).resolve() + role = str(skill_binding_role or "").strip().lower() + ws = Path(workspace_dir).expanduser().resolve() if str(workspace_dir or "").strip() else None + + targets: list[Path] = [] + # session-bootstrap static source files + targets.append((repo / "runtime" / "skills" / "session-bootstrap" / "SOUL.md").resolve()) + targets.append((repo / "runtime" / "skills" / "session-bootstrap" / "IDENTITY.md").resolve()) + # wiki global memory/improvement sources + targets.append((repo / "data" / "wiki" / "improvement" / "learnings.md").resolve()) + targets.append((repo / "data" / "wiki" / "improvement" / "errors.md").resolve()) + targets.append((repo / "data" / "wiki" / "improvement" / "feature-requests.md").resolve()) + + core_dir = (repo / "data" / "wiki" / "core").resolve() + if core_dir.exists() and core_dir.is_dir(): + for p in sorted(core_dir.glob("*.md"), key=lambda x: x.name.lower()): + targets.append(p.resolve()) + + if role: + role_dir = (repo / "data" / "wiki" / "experts" / role).resolve() + if role_dir.exists() and role_dir.is_dir(): + for p in sorted(role_dir.glob("*.md"), key=lambda x: x.name.lower()): + targets.append(p.resolve()) + + if ws is not None: + mem_dir = (ws / "memory").resolve() + if mem_dir.exists() and mem_dir.is_dir(): + mem_files = [p for p in mem_dir.glob("*.md") if p.is_file()] + mem_files.sort(key=lambda p: p.stat().st_mtime if p.exists() else 0.0, reverse=True) + if mem_files: + targets.append(mem_files[0].resolve()) + + h = hashlib.sha256() + for p in targets: + h.update(_file_stat_signature(p).encode("utf-8", errors="ignore")) + h.update(b"\n") + return h.hexdigest()[:24] + + def _executor_prompt_settings_signature(store: Any) -> tuple[str, ...]: keys = ( "AIA_SKILL_RUNTIME_ENABLED", @@ -145,6 +201,7 @@ def get_executor_prompt_static( bool(tools is not None), int(bool(excl)), str(lane_seg or ""), + _session_bootstrap_content_signature(skill_binding_role=skill_binding_role, workspace_dir=workspace_dir), ) with _EXECUTOR_STATIC_PROMPT_CACHE_LOCK: cached = _EXECUTOR_STATIC_PROMPT_CACHE.get(cache_key)