diff --git a/interfaces/admin/static/app.js b/interfaces/admin/static/app.js index ac6b7c41..0d21acdd 100644 --- a/interfaces/admin/static/app.js +++ b/interfaces/admin/static/app.js @@ -1643,10 +1643,27 @@ async function renderStack() { class: "btn btn--primary", text: currentLang === "zh" ? "立即预热" : "Run prewarm now", onclick: async () => { + prewarmStatus.textContent = currentLang === "zh" ? "[预热] 提交中…" : "[prewarm] submitting…"; const resp = await apiPost("/admin/api/runtime/prewarm", { mode: "async", reason: "admin_manual" }); - prewarmStatus.textContent = `[prewarm] accepted=${Boolean(resp && resp.accepted)}`; + if (!(resp && resp.accepted)) { + prewarmStatus.textContent = `[prewarm] accepted=${Boolean(resp && resp.accepted)}`; + return; + } + prewarmStatus.textContent = + currentLang === "zh" + ? "[预热] 后台执行中,完成后自动刷新本页数据(含提示词)…" + : "[prewarm] running in background; refreshing this page when finished…"; runtimePrewarmReminder = ""; - router(); + const maxWaitMs = 120000; + const stepMs = 400; + let waited = 0; + while (waited < maxWaitMs) { + await new Promise((r) => setTimeout(r, stepMs)); + waited += stepMs; + const st = await apiGet("/admin/api/runtime/prewarm/status"); + if (!(st && st.running)) break; + } + await router(); }, }); const prewarmSummary = [ diff --git a/runtime/prompt_prebuild.py b/runtime/prompt_prebuild.py index 4f0e5918..abca7364 100644 --- a/runtime/prompt_prebuild.py +++ b/runtime/prompt_prebuild.py @@ -9,6 +9,7 @@ from oclaw.platform.persistence.sqlite_store import SqliteStore from oclaw.runtime.agent_context import build_role_system_context from oclaw.runtime.agents.specialists import discover_specialist_ids from oclaw.runtime.direct_loop import tool_wire_freeze_status, warm_tool_wire_cache +from oclaw.runtime.skill_role_binding import SKILL_ROLE_BINDING_KEY from oclaw.runtime.system_prompt import get_executor_prompt_static, warm_executor_prompt_cache from oclaw.runtime.tools.catalog import default_registry from oclaw.runtime.workspaces.experts import expert_workspace_signature_token, list_experts @@ -36,6 +37,7 @@ def _manager_settings_signature(store: Any) -> tuple[str, ...]: "AIA_SKILL_DISABLED_NAMES", "AIA_SKILL_ROLE_BINDING_ENABLED", "AIA_SKILL_ROLE_BINDING_MANAGER_INHERIT", + SKILL_ROLE_BINDING_KEY, "AIA_CHAT_SPECIALIST_FLAGS_JSON", ) parts: list[str] = [] diff --git a/runtime/system_prompt.py b/runtime/system_prompt.py index 9f6c4a44..54ea2892 100644 --- a/runtime/system_prompt.py +++ b/runtime/system_prompt.py @@ -8,6 +8,7 @@ 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.skill_role_binding import SKILL_ROLE_BINDING_KEY from oclaw.runtime.skills_prompt import build_skills_catalog_block from oclaw.runtime.skills_workspace_lane import ( fs_safe_workspace_lane_segment, @@ -83,6 +84,7 @@ def _executor_prompt_settings_signature(store: Any) -> tuple[str, ...]: "AIA_SKILL_DISABLED_NAMES", "AIA_SKILL_ROLE_BINDING_ENABLED", "AIA_SKILL_ROLE_BINDING_MANAGER_INHERIT", + SKILL_ROLE_BINDING_KEY, "AIA_PROJECT_CONTEXT_MAX_FILE_CHARS", "AIA_PROJECT_CONTEXT_MAX_TOTAL_CHARS", ) diff --git a/tests/test_oclaw_system_prompt.py b/tests/test_oclaw_system_prompt.py index b915287d..b72db624 100644 --- a/tests/test_oclaw_system_prompt.py +++ b/tests/test_oclaw_system_prompt.py @@ -129,6 +129,67 @@ def test_executor_static_prompt_cache_invalidates_on_settings_change(monkeypatch assert calls["n"] == 2 +def test_executor_static_prompt_cache_invalidates_on_skill_role_binding_change(monkeypatch: pytest.MonkeyPatch) -> None: + from oclaw.runtime import system_prompt as sp + + class DummyStore: + def __init__(self) -> None: + self.values: dict[str, str] = {"skill_role_binding": "{}"} + + def get_setting(self, key: str) -> str: + return str(self.values.get(key, "")) + + class DummyReg: + pass + + calls = {"n": 0} + + def _skills_block(**kwargs) -> str: + _ = kwargs + calls["n"] += 1 + return "skills-block" + + monkeypatch.setattr(sp, "expert_workspace_signature_token", lambda: ("sig",)) + monkeypatch.setattr(sp, "build_project_context_block", lambda **kwargs: "") + monkeypatch.setattr(sp, "build_skills_catalog_block", _skills_block) + monkeypatch.setattr( + sp, + "render_runtime_prompt", + lambda prompt_id, variables, strict: f"{prompt_id}\n{variables.get('skills_catalog') or ''}", + ) + + store = DummyStore() + reg = DummyReg() + _ = sp.get_executor_prompt_static( + store=store, + tools=reg, # type: ignore[arg-type] + base_url="", + base_system="base", + workspace_dir=None, + skill_binding_role="generalist", + ) + _ = sp.get_executor_prompt_static( + store=store, + tools=reg, # type: ignore[arg-type] + base_url="", + base_system="base", + workspace_dir=None, + skill_binding_role="generalist", + ) + assert calls["n"] == 1 + + store.values["skill_role_binding"] = '{"generalist": ["alpha-skill"]}' + _ = sp.get_executor_prompt_static( + store=store, + tools=reg, # type: ignore[arg-type] + base_url="", + base_system="base", + workspace_dir=None, + skill_binding_role="generalist", + ) + assert calls["n"] == 2 + + def test_executor_static_prompt_cache_invalidates_on_workspace_revision_change(monkeypatch: pytest.MonkeyPatch) -> None: from oclaw.runtime import system_prompt as sp