Compare commits

..
Author SHA1 Message Date
kristinheandCursor 409ab9a9b4 fix(memhub): clear submitted memory query with input
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-27 12:22:55 +08:00
8 changed files with 62 additions and 81 deletions

No files matched your search

+3 -15
View File
@@ -340,13 +340,7 @@ def sdk_migrate(options: Sequence[str], stream: TextIO) -> int:
_write_restore(stream, as_json, document, "预览通过;加 --yes 后执行迁移。")
return 4
from one_memory.hy_runtime_config import (
HY_MEMORY_NAMESPACE,
HY_MEMORY_VERSION,
configure_hy_runtime_environment,
)
configure_hy_runtime_environment(settings.data_dir)
from one_memory.hy_runtime_config import HY_MEMORY_NAMESPACE, HY_MEMORY_VERSION
from one_memory.sdk_migration_runner import run_startup_migration
step_names = {
@@ -656,7 +650,6 @@ def serve(*, maintenance: bool = False) -> None:
from one_memory.diagnostic_log import configure_diagnostics, diagnostic_event
from one_memory.hy_runtime_config import (
apply_diagnostic_log_level,
configure_hy_runtime_environment,
diagnostic_log_level,
)
from one_memory.log_file import start_logging_to_file
@@ -664,15 +657,10 @@ def serve(*, maintenance: bool = False) -> None:
from one_memory.runtime import CoreRuntime, parent_pid
settings = load_settings()
if not maintenance:
_refuse_unmigrated_home(settings.data_dir)
# HY resolves some trace paths at import time and its pipeline path when
# SharedRuntime starts. Bind both to the MemHub root before importing the
# application graph; otherwise HY falls back to ~/.hy_memory while the
# desktop Debug panel watches the managed data directory.
configure_hy_runtime_environment(settings.data_dir)
from one_memory.api import create_app
if not maintenance:
_refuse_unmigrated_home(settings.data_dir)
start_logging_to_file(settings.data_dir)
apply_diagnostic_log_level(diagnostic_log_level(settings.data_dir))
configure_diagnostics(
@@ -110,24 +110,11 @@ def test_log_level_document_migrates_the_old_boolean_shape(tmp_path: Path) -> No
assert json.loads(target.read_text()) == {"schema_version": 2, "level": "error"}
def test_configure_environment_passes_debug_content_settings_to_sdk(
tmp_path: Path, monkeypatch
) -> None:
for name in (
"MEMORY_DATA_DIR",
"MEMORY_LOG_DIR",
"MEMORY_LOG_LEVEL",
"MEMORY_PIPELINE_TRACE_ENABLED",
"MEMORY_LOG_LLM_CONTENT",
):
monkeypatch.delenv(name, raising=False)
def test_configure_environment_passes_debug_content_settings_to_sdk(tmp_path: Path) -> None:
set_diagnostic_log_level(tmp_path, "debug")
configure_hy_runtime_environment(tmp_path)
environment = __import__("os").environ
memory_root = tmp_path / "hy-memory" / "governed-v2"
assert environment["MEMORY_DATA_DIR"] == str(memory_root)
assert environment["MEMORY_LOG_DIR"] == str(memory_root / "logs")
assert environment["MEMORY_LOG_LEVEL"] == "DEBUG"
assert environment["MEMORY_PIPELINE_TRACE_ENABLED"] == "true"
assert environment["MEMORY_LOG_LLM_CONTENT"] == "true"
@@ -1,24 +1,18 @@
import inspect
from types import SimpleNamespace
from one_memory import api, cli, hy_runtime_config, log_file, runtime
from one_memory import api, cli, log_file, runtime
def test_production_serve_passes_only_supported_app_options(monkeypatch, tmp_path):
import uvicorn
serve_source = inspect.getsource(cli.serve)
assert serve_source.index("configure_hy_runtime_environment(settings.data_dir)") < (
serve_source.index("from one_memory.api import create_app")
)
settings = SimpleNamespace(data_dir=tmp_path, host="127.0.0.1", port=12345)
captured = {}
startup_order = []
marker = object()
create_app_signature = inspect.signature(api.create_app)
def factory(*args, **kwargs):
startup_order.append("create_app")
create_app_signature.bind(*args, **kwargs)
captured.update(kwargs)
return marker
@@ -33,18 +27,12 @@ def test_production_serve_passes_only_supported_app_options(monkeypatch, tmp_pat
}
monkeypatch.setattr(cli, "load_settings", lambda: settings)
monkeypatch.setattr(
hy_runtime_config,
"configure_hy_runtime_environment",
lambda data_dir: startup_order.append(f"configure_hy:{data_dir}"),
)
monkeypatch.setattr(log_file, "start_logging_to_file", lambda _: None)
monkeypatch.setattr(runtime, "parent_pid", lambda: None)
monkeypatch.setattr(api, "create_app", factory)
monkeypatch.setattr(uvicorn, "run", run)
cli.serve()
assert captured == {"manage_runtime": True}
assert startup_order == [f"configure_hy:{tmp_path}", "create_app"]
def test_maintenance_serve_does_not_own_the_runtime(monkeypatch, tmp_path):
@@ -71,11 +59,6 @@ def test_maintenance_serve_does_not_own_the_runtime(monkeypatch, tmp_path):
}
monkeypatch.setattr(cli, "load_settings", lambda: settings)
monkeypatch.setattr(
hy_runtime_config,
"configure_hy_runtime_environment",
lambda _data_dir: None,
)
monkeypatch.setattr(log_file, "start_logging_to_file", lambda _: None)
monkeypatch.setattr(runtime, "parent_pid", lambda: None)
monkeypatch.setattr(api, "create_app", factory)
@@ -143,24 +143,14 @@ def test_a_finished_migration_records_the_result(home, monkeypatch):
legacy_home(home)
from one_memory.sdk_migration_runner import StartupMigrationResult
startup_order = []
def run_startup_migration(**options):
startup_order.append("run_migration")
options["progress"]("cutover")
return StartupMigrationResult(state="cutover", migration_id="sdk-authority-v2-abc")
import one_memory.hy_runtime_config as hy_runtime_config
import one_memory.sdk_migration_runner as runner
monkeypatch.setattr(
hy_runtime_config,
"configure_hy_runtime_environment",
lambda data_dir: startup_order.append(f"configure_hy:{data_dir}"),
)
monkeypatch.setattr(runner, "run_startup_migration", run_startup_migration)
assert cli.run(MIGRATE, output=StringIO()) == 0
assert startup_order == [f"configure_hy:{home}", "run_migration"]
finished = lines(home)[-1]
assert finished["event"] == "migrate.finished"
assert finished["migration_id"] == "sdk-authority-v2-abc"
+15 -17
View File
@@ -246,23 +246,21 @@ it("shows HY pipeline metrics without exposing model content", async () => {
const target = path.join(pipelineDir, "2026-10-14.log");
await writeFile(target, "");
const cursor = await captureDebugLogCursor(dataDir);
const pipelineEntry = {
request_id: "request-safe-1",
user_id: "private-user",
agent_id: "private-agent",
session_id: "private-session",
step: "EXTRACT",
prompt: "private prompt",
content: "private response",
parsed: "private parsed output",
tokens: 42,
elapsed_ms: 123.5,
timestamp: "2026-10-14T10:01:00.000",
};
await appendFile(target, `${JSON.stringify(pipelineEntry)}\n`);
await writeFile(
path.join(dataDir, "hy-memory", "governed-v2", "logs", "hy_memory.log"),
`${JSON.stringify({ ...pipelineEntry, request_id: "must-not-appear" })}\n`,
await appendFile(
target,
`${JSON.stringify({
request_id: "request-safe-1",
user_id: "private-user",
agent_id: "private-agent",
session_id: "private-session",
step: "EXTRACT",
prompt: "private prompt",
content: "private response",
parsed: "private parsed output",
tokens: 42,
elapsed_ms: 123.5,
timestamp: "2026-10-14T10:01:00.000",
})}\n`,
);
const snapshot = await readDebugLogSnapshot(
+1 -6
View File
@@ -18,7 +18,6 @@ const UI_TRACE = "one-memory:log-ui-trace";
const DEBUG_LOGS = "one-memory:get-debug-logs";
const DEBUG_LOG_NAME =
/^diagnostic-(runtime|core|host-bridge|desktop|cli)\.jsonl(?:\.\d+)?$/;
const PIPELINE_LOG_NAME = /^\d{4}-\d{2}-\d{2}\.log$/;
const SERVICE_LOG_NAMES = [
"service.log",
"service-error.log",
@@ -555,11 +554,7 @@ async function pipelineLogFiles(root: string): Promise<PipelineLogCursor[]> {
await visit(target);
continue;
}
// The same root also contains HY's human-readable hy_memory.log.
// Pipeline files are date-named JSONL; accepting every .log file makes
// the Debug poller repeatedly parse unrelated text and still display no
// HY entries.
if (!entry.isFile() || !PIPELINE_LOG_NAME.test(entry.name)) continue;
if (!entry.isFile() || !entry.name.endsWith(".log")) continue;
const info = await safeRegularFile(target);
if (!info) continue;
files.push({
+29
View File
@@ -223,6 +223,35 @@ it("searches only when 搜索 is pressed, not on every keystroke", async () => {
expect(resetPage).toHaveBeenCalled();
});
it("clearing a submitted keyword immediately shows every memory from the selected source", async () => {
const onQuery = vi.fn();
const onSource = vi.fn();
const resetPage = vi.fn();
render(
<Host
simplified
libraryView={{
...pagedView({ items: [], total: 0, offset: 0, limit: 100 }),
query: "github",
source: "workbuddy",
onQuery,
onSource,
resetPage,
}}
/>,
);
const input = screen.getByRole<HTMLInputElement>("textbox", {
name: "筛选地图记忆",
});
expect(input.value).toBe("github");
await userEvent.clear(input);
expect(onQuery).toHaveBeenCalledExactlyOnceWith("");
expect(resetPage).toHaveBeenCalledOnce();
expect(onSource).not.toHaveBeenCalled();
});
it("OBS-15: switching memory shows a read in progress instead of the previous memory", async () => {
const user = userEvent.setup();
// The read is deliberately never answered: what matters is what is on
+12 -1
View File
@@ -679,7 +679,18 @@ export function MemoryMap(props: Props) {
}}
onChange={(e) => {
if (library) {
setSearchDraft(e.target.value);
const next = e.target.value;
setSearchDraft(next);
// An empty search box means "no keyword filter". Keeping the
// last submitted query active after the reader removed its
// text made the source picker look empty even when that app
// had memories.
if (!next && library.query.trim()) {
setFilterNotice("");
clear();
library.onQuery("");
library.resetPage();
}
return;
}
setFilterNotice("");