Compare commits

...
Author SHA1 Message Date
kristinheandCursor 24f699a726 fix(memhub): surface HY pipeline logs in Debug
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-27 11:43:35 +08:00
6 changed files with 80 additions and 21 deletions

No files matched your search

+15 -3
View File
@@ -340,7 +340,13 @@ 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
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.sdk_migration_runner import run_startup_migration
step_names = {
@@ -650,6 +656,7 @@ 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
@@ -657,10 +664,15 @@ def serve(*, maintenance: bool = False) -> None:
from one_memory.runtime import CoreRuntime, parent_pid
settings = load_settings()
from one_memory.api import create_app
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
start_logging_to_file(settings.data_dir)
apply_diagnostic_log_level(diagnostic_log_level(settings.data_dir))
configure_diagnostics(
@@ -110,11 +110,24 @@ 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) -> None:
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)
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,18 +1,24 @@
import inspect
from types import SimpleNamespace
from one_memory import api, cli, log_file, runtime
from one_memory import api, cli, hy_runtime_config, 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
@@ -27,12 +33,18 @@ 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):
@@ -59,6 +71,11 @@ 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,14 +143,24 @@ 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"
+17 -15
View File
@@ -246,21 +246,23 @@ 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);
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 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`,
);
const snapshot = await readDebugLogSnapshot(
+6 -1
View File
@@ -18,6 +18,7 @@ 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",
@@ -554,7 +555,11 @@ async function pipelineLogFiles(root: string): Promise<PipelineLogCursor[]> {
await visit(target);
continue;
}
if (!entry.isFile() || !entry.name.endsWith(".log")) 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;
const info = await safeRegularFile(target);
if (!info) continue;
files.push({