Compare commits

...
Author SHA1 Message Date
alvinfeiandCursor f73167135a docs(memhub): clarify debug log privacy
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-17 17:16:39 +08:00
alvinfeiandCursor c244e9b2fb feat(memhub): add product memory pipeline diagnostics
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-17 17:16:39 +08:00
7 changed files with 300 additions and 0 deletions

No files matched your search

+2
View File
@@ -42,6 +42,8 @@ Linux 制品由 GitHub Actions 在原生架构 runner 上构建并完成生命
架构详情见 [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) 和
[`docs/contracts/runtime-unification.md`](docs/contracts/runtime-unification.md)。
产品、测试和一线支持排查导入与记忆沉淀时,使用
[`docs/PRODUCT_LOG_TROUBLESHOOTING.md`](docs/PRODUCT_LOG_TROUBLESHOOTING.md)。
## 开发环境
@@ -316,6 +316,10 @@ class ImportSupervisor:
"source_id": current.source_id,
"mode": current.operation,
"count": current.stored,
"stored_count": current.stored,
"revised_count": current.revised,
"skipped_count": current.skipped,
"error_count": current.errors,
},
)
@@ -334,6 +334,19 @@ class MemoryTaskRunner:
**diagnostic_exception(error),
)
raise
outputs, counts = await asyncio.gather(
in_the_background(self.tasks.outputs, task_id),
in_the_background(self.tasks.counts),
)
outstanding = sum(
counts.get(status, 0)
for status in (
MemoryTaskStatus.PENDING,
MemoryTaskStatus.WAITING_FOR_PERMISSION,
MemoryTaskStatus.WAITING_FOR_MODEL,
MemoryTaskStatus.FAILED,
)
)
diagnostic_event(
level="error" if result.status is MemoryTaskStatus.FAILED else "info",
component="memory.task",
@@ -343,6 +356,11 @@ class MemoryTaskRunner:
duration_ms=elapsed_ms(started),
state_after=str(result.status),
error_code=result.last_error_code,
details={
"result_count": len(outputs),
"pending_count": outstanding,
"attempt": result.attempts,
},
)
return result
@@ -165,6 +165,13 @@ def test_import_request_trace_reaches_the_background_job_without_original_text(t
assert "import.queued" in names
assert "import.started" in names
assert "import.finished" in names
finished = next(event for event in events if event["event"] == "import.finished")
assert {
"stored_count",
"revised_count",
"skipped_count",
"error_count",
} <= finished["details"].keys()
assert secret not in written
@@ -315,6 +315,12 @@ def test_memory_task_logs_six_step_durations_without_content(tmp_path) -> None:
events = [json.loads(line) for line in written.splitlines()]
steps = [event for event in events if event["event"] == "memory_task.step_completed"]
assert [event["details"]["phase"] for event in steps] == ["1", "2", "3", "4", "5", "6"]
finished = next(event for event in events if event["event"] == "memory_task.finished")
assert finished["details"] == {
"attempt": 1,
"pending_count": 0,
"result_count": 1,
}
assert tasks.processing_metrics()["hy_calls"] == 1
assert all(event["duration_ms"] >= 0 for event in steps)
assert marker not in written
+253
View File
@@ -0,0 +1,253 @@
# MemHub 产品日志排查手册
本文给产品、测试和一线支持同学使用。目标不是解释底层实现,而是回答三个问题:
1. 数据有没有成功导入;
2. 记忆沉淀有没有持续运行、产出了多少条;
3. 如果停住,停在导入、模型调用还是最终写入。
默认 `info` 下的结构化诊断日志不包含会话正文、模型输入输出或 API Key。`debug`
会额外启用 HY pipeline trace,其中可能包含完整 prompt/response 和获授权的对话正文。
排查时优先通过 App 的“导出日志”提供文件,不要直接发送整个 `~/.one-memory` 数据目录;
debug 期间产生的 pipeline 日志在分享前必须由开发人员确认并脱敏。
## 1. Log level 设什么
### 日常观察:`info`
`info` 是默认级别,已经包含导入数量、任务排队、模型沉淀结果、待处理数量和失败终态。
持续观察产品是否正常工作时,不需要打开 debug。
### 复现具体问题:`debug`
只有开发人员明确要求跟踪一次点击、一次召回或一个具体模型任务时,才打开:
> 设置 → 运行诊断 → 详细日志(排查用)
开关打开后等同于 `debug`。复现完成后:
1. 点击“记录问题时间点”;
2. 点击“导出日志(发给开发排查)”;
3. 关闭“详细日志(排查用)”。
debug 可能把完整模型 prompt/response 写入本机调试日志,不得长期开启。切回 `info`
只会停止后续正文记录,不会自动删除此前已经产生的 debug 文件。
不要把日志级别设置成仅 `warning` 或 `error` 来判断系统是否健康,因为正常进度事件是
`info`。
## 2. 日志在哪里
App 内推荐直接使用“运行诊断 → 导出日志”。开发人员远程指导时,优先使用
`memhub logs`,它会再次过滤不安全字段。需要现场查看原始结构化日志时,Core 日志默认位于:
```text
~/.one-memory/logs/diagnostic-core.jsonl
```
每行都是一条 JSON。macOS 终端中可先设置:
```bash
LOG="$HOME/.one-memory/logs/diagnostic-core.jsonl"
```
## 3. 用什么 key 过滤
产品排查只需要认识以下字段:
- `timestamp`:发生时间;
- `level`:`info`、`warn`、`error`;
- `event`:事件名称,是最重要的过滤 key;
- `outcome`:`started`、`succeeded`、`completed`、`failed` 等结果;
- `error_code`:稳定错误码,可直接发给开发;
- `job_id`:一次导入任务;
- `task_id`:一次记忆沉淀任务;
- `request_id` / `trace_id`:串联一次模型或界面请求;
- `details.result_count`:本次沉淀最终关联了多少条记忆;
- `details.pending_count`:完成本次任务后还有多少条待处理;
- `details.stored_count`:本次导入新增了多少条原始消息;
- `duration_ms`:耗时。
不要按用户原话、会话内容或 API Key 搜索;这些内容按设计不会进入诊断日志。
## 4. 系统正常工作时应该看到什么
### A. Core 已启动
```text
event = core.ready
outcome = succeeded
```
没有 `core.ready` 时,后面的导入和沉淀都不会运行。
### B. 原始消息正在导入
```text
event = import.finished
outcome = completed
details.stored_count >= 0
details.error_count = 0
```
`stored_count=0` 可能只是没有新消息;只要状态不是 failed,就不能单凭 0 判故障。
### C. 记忆任务进入队列
```text
event = memory_scheduler.tasks_queued
outcome = succeeded
details.prepared_count > 0
```
这表示导入的数据已经进入记忆沉淀队列。
### D. SDK 正在沉淀
```text
event = hy.ingest_started
outcome = started
```
随后应出现:
```text
event = hy.ingest_completed
outcome = succeeded
details.result_count = 本次 SDK 返回的记忆数
```
如果有 reconcile,还会出现:
```text
event = hy.reconcile.completed
details.created_count
details.updated_count
details.superseded_count
details.deleted_count
```
### E. MemHub 已完成本次沉淀
最适合产品观察的是:
```text
event = memory_task.finished
outcome = completed
details.result_count = 本任务最终写入或关联的记忆数
details.pending_count = 剩余待处理任务数
details.attempt = 第几次尝试
```
健康表现是:
- `memory_task.finished` 持续出现;
- `pending_count` 总体下降,或在持续导入时保持动态变化;
- `result_count` 间歇性大于 0;
- 没有持续重复的同一 `error_code`。
`result_count=0` 并不一定是故障。某段对话没有值得长期保存的信息时,0 是正常结果。只有大量
任务连续为 0,同时存在 `hy.ingest_failed`、`memory_task.failed` 或积压只增不减,才需要升级
排查。
## 5. 可复制的过滤命令
优先使用以下安全读取命令:
```bash
memhub logs --since 2h --event memory_task.finished --json
memhub logs --since 2h --event hy.reconcile.completed --json
memhub logs --since 24h --errors-only --tail 500 --json
memhub status --json
```
以下 `jq` 命令仅用于 `diagnostic-core.jsonl`,不要对 `core.log`、`service.log` 或 debug
pipeline 日志直接运行后把完整输出发给他人。
### 查看最近的失败
```bash
jq -r '
select(.level == "error" or .outcome == "failed") |
[.timestamp, .service, .event, (.error_code // "-"), (.task_id // .job_id // "-")] |
@tsv
' "$LOG"
```
### 查看每次沉淀数量和剩余积压
```bash
jq -r '
select(.event == "memory_task.finished") |
[.timestamp, .outcome, (.details.result_count // 0),
(.details.pending_count // 0), (.details.attempt // 0), (.error_code // "-")] |
@tsv
' "$LOG"
```
输出列依次是:
```text
时间 结果 本次记忆数 剩余待处理数 尝试次数 错误码
```
### 统计当前日志里累计完成和累计记忆数
```bash
jq -s '
[.[] | select(.event == "memory_task.finished" and .outcome == "completed")] |
{
completed_tasks: length,
memory_results: (map(.details.result_count // 0) | add // 0),
latest_pending: (last.details.pending_count // 0)
}
' "$LOG"
```
这里的 `memory_results` 是任务结果数量之和,用于判断流水线是否持续产出;它不是当前记忆库
去重后的总行数。
### 查看导入数量
```bash
jq -r '
select(.event == "import.finished") |
[.timestamp, .details.source_id, .outcome,
(.details.stored_count // 0), (.details.revised_count // 0),
(.details.skipped_count // 0), (.details.error_count // 0)] |
@tsv
' "$LOG"
```
### 跟踪一个具体任务
先从失败或完成日志复制 `task_id`:
```bash
TASK_ID="粘贴-task_id"
jq --arg id "$TASK_ID" 'select(.task_id == $id)' "$LOG"
```
## 6. 常见停点怎么判断
- 有 `core.ready`,没有 `import.finished`:来源未授权、未开启同步,或来源读取失败。
- `import.finished` 正常,没有 `memory_scheduler.tasks_queued`:导入内容尚未形成完整会话,或未进入处理窗口。
- 有 `memory_task.started`,没有 `hy.ingest_completed`:重点查模型配置、余额、网络、超时和 `hy.ingest_failed`。
- 有 `hy.ingest_completed`,但 `memory_task.finished` 失败:重点查最终治理写入和对应 `error_code`。
- `pending_count` 长时间完全不下降:查看是否反复出现同一个错误码,或任务都在
`waiting_for_model`。
- 导入数量持续增加、沉淀任务完全没有出现:这是异常,不应解释为“对话没有记忆价值”。
## 7. 产品反馈给开发时提供什么
请提供:
1. 问题发生的大致时间;
2. App 版本和平台;
3. 是否开启了详细日志;
4. 点击“记录问题时间点”后显示的结果;
5. 通过“导出日志”得到的诊断文件;
6. 最近重复出现的 `error_code`;
7. `memory_task.finished` 最近几行中的 `result_count` 和 `pending_count`。
不要提供 API Key,也不要打包发送整个记忆数据库。
@@ -44,6 +44,16 @@ host-bridge、desktop 和 cli。单文件 4 MiB,保留 3 份轮转;额外清
权限与安装操作、关键 IPC、失败分类、重试、耗时和终态。不记录无变化的高频轮询或逐条原文,
导入使用阶段和计数汇总,避免日志反过来造成性能问题。
产品进度判断使用以下稳定事件:
- `import.finished`:`stored_count`、`revised_count`、`skipped_count`、`error_count`;
- `hy.ingest.completed`:SDK 本次返回的 `result_count`;
- `hy.reconcile.completed`:新增、更新、取代和删除数量;
- `memory_task.finished`:最终 `result_count`、剩余 `pending_count` 和 `attempt`。
其中 `memory_task.finished` 是产品侧判断沉淀是否持续推进的最终事件;不能只用
`hy.ingest.completed` 代替,因为后者早于 Governance 写入。
## 验收
隐私测试必须先植入一条可检测的秘密作为正向对照,再证明真实链路日志和导出均不含该秘密。