fix(ohmo): isolate personal memory from project memory

This commit is contained in:
tjb-tech
2026-04-29 08:47:58 +00:00
parent df348b6335
commit cc825efb83
13 changed files with 248 additions and 18 deletions
+1
View File
@@ -29,6 +29,7 @@ The format is based on Keep a Changelog, and this project currently tracks chang
- Compaction now detects llama.cpp/OpenAI-compatible context overflow errors, accounts for image blocks in auto-compact token estimates, and strips image payloads from summarizer-only compaction requests.
- Large tool results are now bounded in conversation history: oversized outputs are saved under `tool_artifacts`, old MCP results become microcompactable, and context collapse trims stale tool-result payloads.
- ohmo now keeps personal memory isolated from OpenHarness project memory: `/memory` in ohmo sessions targets the ohmo workspace memory store, and ohmo runtime prompt refreshes no longer inject project memory unless explicitly requested.
- Fixed `glob` and `grep` tools hanging indefinitely when the `rg` subprocess produced enough stderr output to fill the OS pipe buffer. `stderr` is now redirected to `DEVNULL` so it is discarded rather than blocking the child process.
- Fixed `bash_tool` hanging after a timed-out command when the subprocess stdout stream stayed open. `_read_remaining_output` now applies a 2-second `asyncio.wait_for` timeout so the tool always returns promptly.
- Fixed `session_runner` background task deadlock caused by an unread `stderr=PIPE` stream. The subprocess now uses `stderr=STDOUT` so all output merges into the single readable stdout pipe.
+8
View File
@@ -33,6 +33,7 @@ from openharness.prompts import build_runtime_system_prompt
from openharness.ui.runtime import RuntimeBundle, _last_user_text, build_runtime, close_runtime, start_runtime
from ohmo.gateway.config import load_gateway_config
from ohmo.memory import create_memory_command_backend
from ohmo.prompts import build_ohmo_system_prompt
from ohmo.session_storage import OhmoSessionBackend
from ohmo.workspace import get_plugins_dir, get_skills_dir, initialize_workspace
@@ -138,6 +139,8 @@ class OhmoSessionRuntimePool:
restore_tool_metadata=snapshot.get("tool_metadata") if snapshot else None,
extra_skill_dirs=(str(get_skills_dir(self._workspace)),),
extra_plugin_roots=(str(get_plugins_dir(self._workspace)),),
memory_backend=create_memory_command_backend(self._workspace),
include_project_memory=False,
)
if snapshot and snapshot.get("session_id"):
bundle.session_id = str(snapshot["session_id"])
@@ -206,6 +209,8 @@ class OhmoSessionRuntimePool:
session_id=bundle.session_id,
extra_skill_dirs=bundle.extra_skill_dirs,
extra_plugin_roots=bundle.extra_plugin_roots,
memory_backend=create_memory_command_backend(self._workspace),
include_project_memory=False,
),
)
async for update in self._stream_command_result(
@@ -508,6 +513,8 @@ class OhmoSessionRuntimePool:
restore_tool_metadata=getattr(bundle.engine, "tool_metadata", {}) or {},
extra_skill_dirs=(str(get_skills_dir(self._workspace)),),
extra_plugin_roots=(str(get_plugins_dir(self._workspace)),),
memory_backend=create_memory_command_backend(self._workspace),
include_project_memory=False,
)
refreshed.session_id = prior_session_id
await start_runtime(refreshed)
@@ -533,6 +540,7 @@ class OhmoSessionRuntimePool:
latest_user_prompt=latest_user_prompt,
extra_skill_dirs=getattr(bundle, "extra_skill_dirs", ()),
extra_plugin_roots=getattr(bundle, "extra_plugin_roots", ()),
include_project_memory=False,
)
+15
View File
@@ -5,6 +5,8 @@ from __future__ import annotations
from pathlib import Path
from re import sub
from openharness.commands import MemoryCommandBackend
from ohmo.workspace import get_memory_dir, get_memory_index_path
@@ -67,3 +69,16 @@ def load_memory_prompt(workspace: str | Path | None = None, *, max_files: int =
lines.extend(["", f"## {path.name}", "```md", content[:4000], "```"])
return "\n".join(lines)
def create_memory_command_backend(workspace: str | Path | None = None) -> MemoryCommandBackend:
"""Return a ``/memory`` backend bound to ohmo's personal memory store."""
return MemoryCommandBackend(
label="ohmo personal memory",
get_memory_dir=lambda: get_memory_dir(workspace),
get_entrypoint=lambda: get_memory_index_path(workspace),
list_files=lambda: list_memory_files(workspace),
add_entry=lambda title, content: add_memory_entry(workspace, title, content),
remove_entry=lambda name: remove_memory_entry(workspace, name),
)
+5
View File
@@ -14,6 +14,7 @@ from openharness.ui.backend_host import run_backend_host
from openharness.ui.runtime import build_runtime, close_runtime, handle_line, start_runtime
from openharness.ui.react_launcher import _resolve_npm, _resolve_tsx, get_frontend_dir
from ohmo.memory import create_memory_command_backend
from ohmo.prompts import build_ohmo_system_prompt
from ohmo.session_storage import OhmoSessionBackend
from ohmo.workspace import get_plugins_dir, get_skills_dir, initialize_workspace
@@ -54,6 +55,8 @@ async def run_ohmo_backend(
session_backend=OhmoSessionBackend(workspace_root),
extra_skill_dirs=extra_skill_dirs,
extra_plugin_roots=extra_plugin_roots,
memory_backend=create_memory_command_backend(workspace_root),
include_project_memory=False,
)
@@ -160,6 +163,8 @@ async def run_ohmo_print_mode(
enforce_max_turns=max_turns is not None,
extra_skill_dirs=extra_skill_dirs,
extra_plugin_roots=extra_plugin_roots,
memory_backend=create_memory_command_backend(workspace_root),
include_project_memory=False,
)
await start_runtime(bundle)
+2
View File
@@ -4,6 +4,7 @@ from openharness.commands.registry import (
CommandContext,
CommandRegistry,
CommandResult,
MemoryCommandBackend,
SlashCommand,
create_default_command_registry,
)
@@ -12,6 +13,7 @@ __all__ = [
"CommandContext",
"CommandRegistry",
"CommandResult",
"MemoryCommandBackend",
"SlashCommand",
"create_default_command_registry",
]
+67 -13
View File
@@ -75,6 +75,18 @@ class CommandResult:
submit_model: str | None = None
@dataclass(frozen=True)
class MemoryCommandBackend:
"""Storage backend used by the generic ``/memory`` slash command."""
label: str
get_memory_dir: Callable[[], Path]
get_entrypoint: Callable[[], Path]
list_files: Callable[[], list[Path]]
add_entry: Callable[[str, str], Path]
remove_entry: Callable[[str], bool]
@dataclass
class CommandContext:
"""Context available to command handlers."""
@@ -90,6 +102,8 @@ class CommandContext:
session_id: str | None = None
extra_skill_dirs: Iterable[str | Path] | None = None
extra_plugin_roots: Iterable[str | Path] | None = None
memory_backend: MemoryCommandBackend | None = None
include_project_memory: bool = True
CommandHandler = Callable[[str, CommandContext], Awaitable[CommandResult]]
@@ -286,7 +300,11 @@ def create_default_command_registry(
async def _context_handler(_: str, context: CommandContext) -> CommandResult:
settings = load_settings()
prompt = build_runtime_system_prompt(settings, cwd=context.cwd)
prompt = build_runtime_system_prompt(
settings,
cwd=context.cwd,
include_project_memory=context.include_project_memory,
)
return CommandResult(message=prompt)
async def _summary_handler(args: str, context: CommandContext) -> CommandResult:
@@ -360,7 +378,8 @@ def create_default_command_registry(
async def _stats_handler(_: str, context: CommandContext) -> CommandResult:
settings = load_settings()
memory_count = len(list_memory_files(context.cwd))
memory_backend = _memory_backend_for_context(context)
memory_count = len(memory_backend.list_files())
task_count = len(get_task_manager().list_tasks())
tool_count = len(context.tool_registry.list_tools()) if context.tool_registry is not None else 0
style = settings.output_style
@@ -380,25 +399,28 @@ def create_default_command_registry(
)
async def _memory_handler(args: str, context: CommandContext) -> CommandResult:
backend = _memory_backend_for_context(context)
tokens = args.split(maxsplit=1)
if not tokens:
memory_dir = get_project_memory_dir(context.cwd)
entrypoint = get_memory_entrypoint(context.cwd)
return CommandResult(
message=f"Memory directory: {memory_dir}\nEntrypoint: {entrypoint}"
message=(
f"Memory store: {backend.label}\n"
f"Memory directory: {backend.get_memory_dir()}\n"
f"Entrypoint: {backend.get_entrypoint()}"
)
)
action = tokens[0]
rest = tokens[1] if len(tokens) == 2 else ""
if action == "list":
memory_files = list_memory_files(context.cwd)
memory_files = backend.list_files()
if not memory_files:
return CommandResult(message="No memory files.")
return CommandResult(message="\n".join(path.name for path in memory_files))
if action == "show" and rest:
memory_dir = get_project_memory_dir(context.cwd)
memory_dir = backend.get_memory_dir()
path, invalid = _resolve_memory_entry_path(memory_dir, rest)
if invalid:
return CommandResult(message="Memory entry path must stay within the project memory directory.")
return CommandResult(message="Memory entry path must stay within the configured memory directory.")
if path is None:
return CommandResult(message=f"Memory entry not found: {rest}")
if not path.exists():
@@ -408,10 +430,10 @@ def create_default_command_registry(
title, separator, content = rest.partition("::")
if not separator or not title.strip() or not content.strip():
return CommandResult(message="Usage: /memory add TITLE :: CONTENT")
path = add_memory_entry(context.cwd, title.strip(), content.strip())
path = backend.add_entry(title.strip(), content.strip())
return CommandResult(message=f"Added memory entry {path.name}")
if action == "remove" and rest:
if remove_memory_entry(context.cwd, rest.strip()):
if backend.remove_entry(rest.strip()):
return CommandResult(message=f"Removed memory entry {rest.strip()}")
return CommandResult(message=f"Memory entry not found: {rest.strip()}")
return CommandResult(message="Usage: /memory [list|show NAME|add TITLE :: CONTENT|remove NAME]")
@@ -508,7 +530,11 @@ def create_default_command_registry(
snapshot_path = context.session_backend.save_snapshot(
cwd=context.cwd,
model=context.app_state.get().model if context.app_state is not None else load_settings().model,
system_prompt=build_runtime_system_prompt(load_settings(), cwd=context.cwd),
system_prompt=build_runtime_system_prompt(
load_settings(),
cwd=context.cwd,
include_project_memory=context.include_project_memory,
),
messages=context.engine.messages,
usage=context.engine.total_usage,
)
@@ -853,7 +879,13 @@ def create_default_command_registry(
return CommandResult(message="Usage: /effort [show|low|medium|high]")
settings.effort = value
save_settings(settings)
context.engine.set_system_prompt(build_runtime_system_prompt(settings, cwd=context.cwd))
context.engine.set_system_prompt(
build_runtime_system_prompt(
settings,
cwd=context.cwd,
include_project_memory=context.include_project_memory,
)
)
if context.app_state is not None:
context.app_state.set(effort=value)
return CommandResult(message=f"Reasoning effort set to {value}.")
@@ -870,7 +902,13 @@ def create_default_command_registry(
return CommandResult(message="Usage: /passes [show|COUNT]")
settings.passes = passes
save_settings(settings)
context.engine.set_system_prompt(build_runtime_system_prompt(settings, cwd=context.cwd))
context.engine.set_system_prompt(
build_runtime_system_prompt(
settings,
cwd=context.cwd,
include_project_memory=context.include_project_memory,
)
)
if context.app_state is not None:
context.app_state.set(passes=passes)
return CommandResult(message=f"Pass count set to {passes}.")
@@ -1983,6 +2021,22 @@ def _resolve_memory_entry_path(memory_dir: Path, candidate: str) -> tuple[Path |
return None, False
def _memory_backend_for_context(context: CommandContext) -> MemoryCommandBackend:
"""Return the active slash-command memory backend for this command context."""
if context.memory_backend is not None:
return context.memory_backend
cwd = context.cwd
return MemoryCommandBackend(
label="OpenHarness project memory",
get_memory_dir=lambda: get_project_memory_dir(cwd),
get_entrypoint=lambda: get_memory_entrypoint(cwd),
list_files=lambda: list_memory_files(cwd),
add_entry=lambda title, content: add_memory_entry(cwd, title, content),
remove_entry=lambda name: remove_memory_entry(cwd, name),
)
def _resolve_memory_candidate(memory_dir: Path, candidate: str) -> tuple[Path | None, bool]:
path = Path(candidate).expanduser()
if not path.is_absolute():
+2 -1
View File
@@ -78,6 +78,7 @@ def build_runtime_system_prompt(
latest_user_prompt: str | None = None,
extra_skill_dirs: Iterable[str | Path] | None = None,
extra_plugin_roots: Iterable[str | Path] | None = None,
include_project_memory: bool = True,
) -> str:
"""Build the runtime system prompt with project instructions and memory."""
if is_coordinator_mode():
@@ -130,7 +131,7 @@ def build_runtime_system_prompt(
if content:
sections.append(f"# {title}\n\n```md\n{content[:12000]}\n```")
if settings.memory.enabled:
if include_project_memory and settings.memory.enabled:
memory_section = load_memory_prompt(
cwd,
max_entrypoint_lines=settings.memory.max_entrypoint_lines,
+9
View File
@@ -15,6 +15,7 @@ from uuid import uuid4
from openharness.api.client import SupportsStreamingMessages
from openharness.auth.manager import AuthManager
from openharness.commands import MemoryCommandBackend
from openharness.config.settings import CLAUDE_MODEL_ALIAS_OPTIONS, resolve_model_setting
from openharness.bridge import get_bridge_manager
from openharness.coordinator.coordinator_mode import is_coordinator_mode
@@ -63,6 +64,8 @@ class BackendHostConfig:
session_backend: SessionBackend | None = None
extra_skill_dirs: tuple[str, ...] = ()
extra_plugin_roots: tuple[str, ...] = ()
memory_backend: MemoryCommandBackend | None = None
include_project_memory: bool = True
class ReactBackendHost:
@@ -102,6 +105,8 @@ class ReactBackendHost:
session_backend=self._config.session_backend,
extra_skill_dirs=self._config.extra_skill_dirs,
extra_plugin_roots=self._config.extra_plugin_roots,
memory_backend=self._config.memory_backend,
include_project_memory=self._config.include_project_memory,
)
await start_runtime(self._bundle)
await self._emit(
@@ -800,6 +805,8 @@ async def run_backend_host(
session_backend: SessionBackend | None = None,
extra_skill_dirs: tuple[str | Path, ...] = (),
extra_plugin_roots: tuple[str | Path, ...] = (),
memory_backend: MemoryCommandBackend | None = None,
include_project_memory: bool = True,
) -> int:
"""Run the structured React backend host."""
if cwd:
@@ -822,6 +829,8 @@ async def run_backend_host(
session_backend=session_backend,
extra_skill_dirs=tuple(str(Path(path).expanduser().resolve()) for path in extra_skill_dirs),
extra_plugin_roots=tuple(str(Path(path).expanduser().resolve()) for path in extra_plugin_roots),
memory_backend=memory_backend,
include_project_memory=include_project_memory,
)
)
return await host.run()
+1
View File
@@ -134,6 +134,7 @@ async def submit_follow_up(
latest_user_prompt=prompt_seed,
extra_skill_dirs=bundle.extra_skill_dirs,
extra_plugin_roots=bundle.extra_plugin_roots,
include_project_memory=bundle.include_project_memory,
)
bundle.engine.set_system_prompt(system_prompt)
try:
+13 -1
View File
@@ -14,7 +14,7 @@ from openharness.api.copilot_client import CopilotClient
from openharness.api.openai_client import OpenAICompatibleClient
from openharness.api.provider import auth_status, detect_provider
from openharness.bridge import get_bridge_manager
from openharness.commands import CommandContext, CommandResult, create_default_command_registry
from openharness.commands import CommandContext, CommandResult, MemoryCommandBackend, create_default_command_registry
from openharness.config import get_config_file_path, load_settings
from openharness.engine import QueryEngine
from openharness.engine.messages import (
@@ -63,6 +63,8 @@ class RuntimeBundle:
session_backend: SessionBackend = DEFAULT_SESSION_BACKEND
extra_skill_dirs: tuple[str, ...] = ()
extra_plugin_roots: tuple[str, ...] = ()
memory_backend: MemoryCommandBackend | None = None
include_project_memory: bool = True
def current_settings(self):
"""Return the effective settings for this session.
@@ -188,6 +190,8 @@ async def build_runtime(
permission_mode: str | None = None,
extra_skill_dirs: Iterable[str | Path] | None = None,
extra_plugin_roots: Iterable[str | Path] | None = None,
memory_backend: MemoryCommandBackend | None = None,
include_project_memory: bool = True,
) -> RuntimeBundle:
"""Build the shared runtime for an OpenHarness session."""
settings_overrides: dict[str, Any] = {
@@ -260,6 +264,7 @@ async def build_runtime(
latest_user_prompt=prompt,
extra_skill_dirs=normalized_skill_dirs,
extra_plugin_roots=normalized_plugin_roots,
include_project_memory=include_project_memory,
)
from uuid import uuid4
@@ -348,6 +353,8 @@ async def build_runtime(
session_backend=session_backend or DEFAULT_SESSION_BACKEND,
extra_skill_dirs=normalized_skill_dirs,
extra_plugin_roots=normalized_plugin_roots,
memory_backend=memory_backend,
include_project_memory=include_project_memory,
)
@@ -515,6 +522,8 @@ async def handle_line(
session_id=bundle.session_id,
extra_skill_dirs=bundle.extra_skill_dirs,
extra_plugin_roots=bundle.extra_plugin_roots,
memory_backend=bundle.memory_backend,
include_project_memory=bundle.include_project_memory,
),
)
if result.refresh_runtime:
@@ -532,6 +541,7 @@ async def handle_line(
latest_user_prompt=submit_prompt,
extra_skill_dirs=bundle.extra_skill_dirs,
extra_plugin_roots=bundle.extra_plugin_roots,
include_project_memory=bundle.include_project_memory,
)
bundle.engine.set_system_prompt(system_prompt)
try:
@@ -564,6 +574,7 @@ async def handle_line(
latest_user_prompt=_last_user_text(bundle.engine.messages),
extra_skill_dirs=bundle.extra_skill_dirs,
extra_plugin_roots=bundle.extra_plugin_roots,
include_project_memory=bundle.include_project_memory,
)
bundle.engine.set_system_prompt(system_prompt)
turns = result.continue_turns if result.continue_turns is not None else bundle.engine.max_turns
@@ -596,6 +607,7 @@ async def handle_line(
latest_user_prompt=line,
extra_skill_dirs=bundle.extra_skill_dirs,
extra_plugin_roots=bundle.extra_plugin_roots,
include_project_memory=bundle.include_project_memory,
)
bundle.engine.set_system_prompt(system_prompt)
try:
+1 -1
View File
@@ -158,7 +158,7 @@ async def test_memory_show_rejects_path_traversal(tmp_path: Path, monkeypatch):
result = await command.handler(args, CommandContext(engine=_make_engine(tmp_path), cwd=str(tmp_path)))
assert result.message == "Memory entry path must stay within the project memory directory."
assert result.message == "Memory entry path must stay within the configured memory directory."
@pytest.mark.asyncio
+99
View File
@@ -14,14 +14,19 @@ from openharness.channels.bus.events import InboundMessage
from openharness.channels.bus.queue import MessageBus
from openharness.commands import CommandResult
from openharness.commands.registry import SlashCommand, create_default_command_registry
from openharness.config.settings import Settings
from openharness.engine.messages import ConversationMessage, ImageBlock, TextBlock, ToolUseBlock
from openharness.engine.stream_events import AssistantTextDelta, CompactProgressEvent, ToolExecutionStarted
from openharness.memory import add_memory_entry as add_project_memory_entry
from openharness.memory import list_memory_files as list_project_memory_files
from ohmo.gateway.bridge import OhmoGatewayBridge, _format_gateway_error
from ohmo.gateway.config import save_gateway_config
from ohmo.gateway.models import GatewayConfig, GatewayState
from ohmo.gateway.runtime import OhmoSessionRuntimePool, _build_inbound_user_message, _format_channel_progress
from ohmo.gateway.service import OhmoGatewayService, gateway_status, stop_gateway_process
from ohmo.memory import add_memory_entry as add_ohmo_memory_entry
from ohmo.memory import list_memory_files as list_ohmo_memory_files
from ohmo.gateway.router import session_key_for_message
from ohmo.session_storage import save_session_snapshot
from ohmo.workspace import get_gateway_restart_notice_path, initialize_workspace
@@ -236,6 +241,7 @@ async def test_runtime_pool_stream_message_emits_progress_and_tool_hint(tmp_path
return SimpleNamespace(
engine=FakeEngine(),
cwd=str(tmp_path),
session_id="sess123",
current_settings=lambda: SimpleNamespace(model="gpt-5.4"),
commands=SimpleNamespace(lookup=lambda raw: None),
@@ -279,6 +285,7 @@ async def test_runtime_pool_stream_message_formats_auto_compact_status_for_feish
return SimpleNamespace(
engine=FakeEngine(),
cwd=str(tmp_path),
session_id="sess123",
current_settings=lambda: SimpleNamespace(model="gpt-5.4"),
commands=SimpleNamespace(lookup=lambda raw: None),
@@ -536,6 +543,7 @@ async def test_runtime_pool_blocks_registered_bridge_spawn_without_shelling_out(
return SimpleNamespace(
engine=FakeEngine(),
cwd=str(tmp_path),
session_id="sess123",
current_settings=lambda: SimpleNamespace(model="gpt-5.4"),
commands=registry,
@@ -559,6 +567,97 @@ async def test_runtime_pool_blocks_registered_bridge_spawn_without_shelling_out(
assert marker.exists() is False
@pytest.mark.asyncio
async def test_runtime_pool_memory_command_uses_ohmo_personal_memory(tmp_path, monkeypatch):
workspace = tmp_path / ".ohmo-home"
initialize_workspace(workspace)
registry = create_default_command_registry()
async def fake_build_runtime(**kwargs):
class FakeEngine:
messages = []
total_usage = UsageSnapshot()
def set_system_prompt(self, prompt):
self.system_prompt = prompt
return SimpleNamespace(
engine=FakeEngine(),
cwd=str(tmp_path),
session_id="sess123",
current_settings=lambda: SimpleNamespace(model="gpt-5.4"),
commands=registry,
tool_registry=None,
app_state=None,
session_backend=None,
extra_skill_dirs=(),
extra_plugin_roots=(),
hook_summary=lambda: "",
mcp_summary=lambda: "",
plugin_summary=lambda: "",
)
async def fake_start_runtime(bundle):
return None
monkeypatch.setenv("OPENHARNESS_CONFIG_DIR", str(tmp_path / "config"))
monkeypatch.setenv("OPENHARNESS_DATA_DIR", str(tmp_path / "data"))
monkeypatch.setattr("ohmo.gateway.runtime.build_runtime", fake_build_runtime)
monkeypatch.setattr("ohmo.gateway.runtime.start_runtime", fake_start_runtime)
pool = OhmoSessionRuntimePool(cwd=tmp_path, workspace=workspace, provider_profile="codex")
message = InboundMessage(
channel="feishu",
sender_id="u1",
chat_id="c1",
content="/memory add Profile :: prefers concise answers",
)
updates = [u async for u in pool.stream_message(message, "feishu:c1")]
assert updates[-1].text == "Added memory entry profile.md"
assert [path.name for path in list_ohmo_memory_files(workspace)] == ["profile.md"]
assert "prefers concise answers" in (workspace / "memory" / "profile.md").read_text(encoding="utf-8")
assert list_project_memory_files(tmp_path) == []
@pytest.mark.asyncio
async def test_runtime_pool_prompt_excludes_project_memory(tmp_path, monkeypatch):
workspace = tmp_path / ".ohmo-home"
initialize_workspace(workspace)
add_ohmo_memory_entry(workspace, "personal", "ohmo-only personal fact")
monkeypatch.setenv("OPENHARNESS_CONFIG_DIR", str(tmp_path / "config"))
monkeypatch.setenv("OPENHARNESS_DATA_DIR", str(tmp_path / "data"))
add_project_memory_entry(tmp_path, "project", "project memory should not leak")
async def fake_build_runtime(**kwargs):
class FakeEngine:
messages = []
total_usage = UsageSnapshot()
def set_system_prompt(self, prompt):
self.system_prompt = prompt
engine = FakeEngine()
return SimpleNamespace(
engine=engine,
session_id="sess123",
current_settings=lambda: Settings(system_prompt=kwargs["system_prompt"]),
commands=create_default_command_registry(),
)
async def fake_start_runtime(bundle):
return None
monkeypatch.setattr("ohmo.gateway.runtime.build_runtime", fake_build_runtime)
monkeypatch.setattr("ohmo.gateway.runtime.start_runtime", fake_start_runtime)
pool = OhmoSessionRuntimePool(cwd=tmp_path, workspace=workspace, provider_profile="codex")
bundle = await pool.get_bundle("feishu:c1", latest_user_prompt="hello")
assert "ohmo-only personal fact" in bundle.engine.system_prompt
assert "project memory should not leak" not in bundle.engine.system_prompt
@pytest.mark.asyncio
async def test_runtime_pool_allows_opted_in_remote_admin_commands(tmp_path, monkeypatch, caplog):
workspace = tmp_path / ".ohmo-home"
+25 -2
View File
@@ -1,6 +1,10 @@
from pathlib import Path
from ohmo.memory import add_memory_entry
from openharness.config.settings import Settings
from openharness.memory import add_memory_entry as add_project_memory_entry
from openharness.prompts import build_runtime_system_prompt
from ohmo.memory import add_memory_entry as add_ohmo_memory_entry
from ohmo.prompts import build_ohmo_system_prompt
from ohmo.workspace import (
get_bootstrap_path,
@@ -18,7 +22,7 @@ def test_ohmo_prompt_includes_persona_and_memory(tmp_path: Path):
get_identity_path(workspace).write_text("# identity\nName: ohmo\n", encoding="utf-8")
get_user_path(workspace).write_text("# user\nPrefers terse answers.\n", encoding="utf-8")
get_bootstrap_path(workspace).write_text("# bootstrap\nAsk a few high-value questions.\n", encoding="utf-8")
add_memory_entry(workspace, "timezone", "The user prefers UTC timestamps.")
add_ohmo_memory_entry(workspace, "timezone", "The user prefers UTC timestamps.")
prompt = build_ohmo_system_prompt(tmp_path, workspace=workspace)
@@ -29,3 +33,22 @@ def test_ohmo_prompt_includes_persona_and_memory(tmp_path: Path):
assert "Ask a few high-value questions." in prompt
assert "timezone.md" in prompt
assert "UTC timestamps" in prompt
def test_ohmo_runtime_prompt_can_exclude_project_memory(tmp_path: Path, monkeypatch):
monkeypatch.setenv("OPENHARNESS_DATA_DIR", str(tmp_path / "data"))
workspace = tmp_path / ".ohmo-home"
initialize_workspace(workspace)
add_ohmo_memory_entry(workspace, "personal", "ohmo-only personal fact")
add_project_memory_entry(tmp_path, "project", "project memory should not leak")
base_prompt = build_ohmo_system_prompt(tmp_path, workspace=workspace)
runtime_prompt = build_runtime_system_prompt(
Settings(system_prompt=base_prompt),
cwd=tmp_path,
latest_user_prompt="hello",
include_project_memory=False,
)
assert "ohmo-only personal fact" in runtime_prompt
assert "project memory should not leak" not in runtime_prompt