openclaw-diag-cli 1.10.4 → 1.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +95 -0
- package/README.md +1 -0
- package/ocdiag/__init__.py +1 -1
- package/ocdiag/inspectors/trace.py +394 -76
- package/ocdiag/main.py +284 -94
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,100 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v1.12.1 — --help / list / examples output switched to English (2026-06-18)
|
|
4
|
+
|
|
5
|
+
`v1.12.0` made the help comprehensive but in Chinese. This patch translates
|
|
6
|
+
every custom help string surfaced by `--help` (top-level + each subcommand +
|
|
7
|
+
each collector/inspector), `openclaw-diag list`, and `openclaw-diag examples`
|
|
8
|
+
to clear English. argparse framework strings (`usage:`, `options:`, `-h, --help`)
|
|
9
|
+
were already English and are unchanged.
|
|
10
|
+
|
|
11
|
+
### Changes
|
|
12
|
+
|
|
13
|
+
Only `ocdiag/main.py` + `tests/test_cli_help.py` are touched. Collectors /
|
|
14
|
+
inspectors business logic, collector `title` attributes, and report rendering
|
|
15
|
+
are untouched.
|
|
16
|
+
|
|
17
|
+
1. `_COMMAND_DESC` values translated to English; `_desc()` unchanged.
|
|
18
|
+
2. `_common_arguments` group renamed to `"global options"`; every flag
|
|
19
|
+
(`--config` / `--log-dir` / `--sessions-base` / `--openclaw-home` /
|
|
20
|
+
`--format` / `--json` / `--no-color` / `--unmask`) carries an English help
|
|
21
|
+
string. The `channel options` group + `--account` help are also English.
|
|
22
|
+
3. `trace` / `extract` / `panorama` parsers: per-command group headers
|
|
23
|
+
(`trace options`, `extract options`, `panorama options`), every flag's
|
|
24
|
+
help text, and the `Examples:` epilog blocks are all English. Description
|
|
25
|
+
line uses the English `_desc()` only — the Chinese collector `title` no
|
|
26
|
+
longer leaks into help.
|
|
27
|
+
4. `_print_help()` rewritten in English (intro / Usage / Health checks / Scan
|
|
28
|
+
diagnostics / Object diagnostics / Helper commands / Global options /
|
|
29
|
+
Exit codes / More).
|
|
30
|
+
5. `cmd_list` pretty + json outputs are fully English. The json `label` field
|
|
31
|
+
now mirrors the English description (no Chinese title leak).
|
|
32
|
+
6. `cmd_examples()` translated; example commands themselves are unchanged.
|
|
33
|
+
7. Per-collector `<id> --help` description switched from
|
|
34
|
+
`f"{coll.title} ({coll.id}) — {desc}"` to `f"{desc} ({coll.id})"` so the
|
|
35
|
+
Chinese title is never printed.
|
|
36
|
+
8. Error messages: `"Error: 未知 ..."` -> `"Error: unknown ..."` and the
|
|
37
|
+
"run `openclaw-diag list`..." hint translated.
|
|
38
|
+
|
|
39
|
+
### Tests
|
|
40
|
+
|
|
41
|
+
`tests/test_cli_help.py`: every assertion that pinned a Chinese substring is
|
|
42
|
+
swapped to its English equivalent. Test intent is preserved 1:1.
|
|
43
|
+
|
|
44
|
+
## v1.12.0 — --help 全面改造:命令/参数清晰说明 + 分组 + 退出码 (2026-06-18)
|
|
45
|
+
|
|
46
|
+
让 `--help`(顶层 + 每个子命令 + 每个 collector/inspector)对**用户和 agent 都
|
|
47
|
+
清晰、明确、直接**。每个命令、每个参数都有一句话说明,零猜测。
|
|
48
|
+
|
|
49
|
+
### 改动
|
|
50
|
+
|
|
51
|
+
唯一业务改动文件:`ocdiag/main.py`(外加 `tests/test_cli_help.py` 新增测试)。
|
|
52
|
+
不动 collectors/inspectors 业务逻辑、不动默认路径/参数 default 值。
|
|
53
|
+
|
|
54
|
+
1. 新增集中式命令描述表 `_COMMAND_DESC`(id → 一句话中文)+ helper `_desc(mid)`,
|
|
55
|
+
供 `list`、`<id> --help` 的 description、顶层 help 复用,未来新增 collector
|
|
56
|
+
缺失时回退空串、不报错。
|
|
57
|
+
2. `_common_arguments` 内全局参数放进 `argument group "全局选项 (global options)"`,
|
|
58
|
+
每个 flag 都补了清晰中文 help(`--config` / `--log-dir` / `--sessions-base` /
|
|
59
|
+
`--openclaw-home` / `--format` / `--json` / `--no-color` / `--unmask`),路径
|
|
60
|
+
类参数加 `metavar=PATH` 让 usage 行更整齐。`--account` 同样进 `"channel 选项"`
|
|
61
|
+
group。
|
|
62
|
+
3. `trace --help` 之前 `--no-trajectory` / `--no-log` / `--show-tool-metas` /
|
|
63
|
+
`--show-plugin-snapshot` / `--mask` 都是裸 flag,本次补齐每个的中文说明,
|
|
64
|
+
并把 trace/extract/panorama 的命令专属参数各自放进 `"trace/extract/panorama 选项"`
|
|
65
|
+
group。
|
|
66
|
+
4. 顶层 `_print_help()` 重写:工具简介 → 用法 → 体检命令 → 扫描类(动态从
|
|
67
|
+
registry 渲染,对齐排版)→ 对象诊断 → 辅助命令 → 全局选项 → 退出码
|
|
68
|
+
(0/1/2/3) → 更多。
|
|
69
|
+
5. `cmd_list` pretty 输出每项追加 `— 描述`;`list --format json` 在每项里加
|
|
70
|
+
`description` 字段,便于 agent / 脚本消费。
|
|
71
|
+
6. 各 collector parser 的 `description` 从 `f"{title} ({id})"` 改为
|
|
72
|
+
`f"{title} ({id}) — {_desc(id)}"`,`<id> --help` 顶部就能看到一句话简介。
|
|
73
|
+
|
|
74
|
+
### Tests
|
|
75
|
+
|
|
76
|
+
`tests/test_cli_help.py` 追加 8 个用例:
|
|
77
|
+
- 顶层 help 含工具简介 / 体检命令 / 扫描类 / 对象诊断 / 辅助命令 / 全局选项 /
|
|
78
|
+
退出码(含 0/1/2/3)等关键 section 标题。
|
|
79
|
+
- 顶层 help 遍历 registry 断言每个 state collector id 出现,未来新增不漏排。
|
|
80
|
+
- 顶层 help 含 trace/extract/panorama 三个 inspector。
|
|
81
|
+
- `trace --help` 五个之前的裸 flag 都带中文 help 片段,且分组标题 `trace 选项`
|
|
82
|
+
+ description 一句话简介都在。
|
|
83
|
+
- `gateway --help` 含 `全局选项` 分组,`--config` / `--unmask` / `--no-color` /
|
|
84
|
+
`--log-dir` / `--sessions-base` / `--openclaw-home` 都带中文说明。
|
|
85
|
+
- `gateway --help` description 拼上 `_COMMAND_DESC['gateway']` 一句话片段。
|
|
86
|
+
- `list` pretty 含已知 collector 的描述片段;`list --format json` 每项含
|
|
87
|
+
`description` key(已登记 id 描述非空)。
|
|
88
|
+
|
|
89
|
+
`pytest tests/ -q` → 234 passed, 1 skipped。
|
|
90
|
+
|
|
91
|
+
### 行为不变
|
|
92
|
+
|
|
93
|
+
- 默认参数值(paths.\*)、各 collector/inspector 业务逻辑、退出码定义、
|
|
94
|
+
argv 解析顺序、扫描结果与 v1.11.x 完全一致。
|
|
95
|
+
- 之前 215+ 用例全绿;本次只增不改测试。
|
|
96
|
+
|
|
97
|
+
|
|
3
98
|
## v1.10.4 — plugin_diag windowed scan filters to dated-in-window (2026-06-16)
|
|
4
99
|
|
|
5
100
|
Minimal correctness hardening of the v1.10.2 full-cache reuse. The 7d/30d
|
package/README.md
CHANGED
|
@@ -125,6 +125,7 @@ openclaw-diag examples # Show usage examples
|
|
|
125
125
|
openclaw-diag trace <uuid> # Last user message
|
|
126
126
|
openclaw-diag trace <uuid> --msg-index 0 # First message
|
|
127
127
|
openclaw-diag trace <uuid> --msg-match "deploy" # Match by content
|
|
128
|
+
openclaw-diag trace <uuid> --all-messages # Every user turn, one block each
|
|
128
129
|
openclaw-diag trace <uuid> --no-trajectory # Skip trajectory enrichment
|
|
129
130
|
|
|
130
131
|
# Extract: dump session content
|
package/ocdiag/__init__.py
CHANGED
|
@@ -26,6 +26,7 @@ from ..core.types import Report, Section, Verdict
|
|
|
26
26
|
from ..tracing import (
|
|
27
27
|
analyze_phases,
|
|
28
28
|
build_system_prompt_info,
|
|
29
|
+
extract_text,
|
|
29
30
|
extract_trace_records,
|
|
30
31
|
find_first_message,
|
|
31
32
|
find_gateway_logs,
|
|
@@ -370,6 +371,152 @@ def _section_gateway(s: Section, gw: Dict[str, Any]) -> None:
|
|
|
370
371
|
)
|
|
371
372
|
|
|
372
373
|
|
|
374
|
+
def _user_text_snippet(user_rec: Dict[str, Any], max_chars: int = 80) -> str:
|
|
375
|
+
"""One-line preview of a user message's text content.
|
|
376
|
+
|
|
377
|
+
Used in the per-turn meta header when --all-messages is on so the reader
|
|
378
|
+
can tell turns apart at a glance. Whitespace is collapsed to single
|
|
379
|
+
spaces; the result is truncated with an ellipsis.
|
|
380
|
+
"""
|
|
381
|
+
msg = user_rec.get("message") or {}
|
|
382
|
+
text = extract_text(msg.get("content", "")) or ""
|
|
383
|
+
text = " ".join(text.split())
|
|
384
|
+
if len(text) > max_chars:
|
|
385
|
+
text = text[:max_chars - 1] + "…"
|
|
386
|
+
return text
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _build_turn_sections(
|
|
390
|
+
report: Report,
|
|
391
|
+
*,
|
|
392
|
+
analysis: Dict[str, Any],
|
|
393
|
+
user_msg_ordinal: int,
|
|
394
|
+
user_msg_id: str,
|
|
395
|
+
total_user_msgs: int,
|
|
396
|
+
turn_label: str,
|
|
397
|
+
user_snippet: str,
|
|
398
|
+
) -> None:
|
|
399
|
+
"""Append the per-turn timeline / summary / model / tool sections.
|
|
400
|
+
|
|
401
|
+
``turn_label`` is prepended to each section title so multi-turn output
|
|
402
|
+
keeps the per-turn blocks visually delimited (e.g. ``"Turn #2/5 · "``).
|
|
403
|
+
|
|
404
|
+
Per-turn vs session-level enrichment split (see TraceInspector.collect):
|
|
405
|
+
Per-turn here = timeline + summary + model breakdown + tool breakdown.
|
|
406
|
+
Trajectory + System Prompt + Gateway are session-scoped (one per file)
|
|
407
|
+
and are emitted ONCE by the caller, not per turn.
|
|
408
|
+
"""
|
|
409
|
+
s_meta = report.section(f"{turn_label}Trace · 元信息")
|
|
410
|
+
s_meta.ok(
|
|
411
|
+
"user_message",
|
|
412
|
+
f"user message #{user_msg_ordinal} of {total_user_msgs} "
|
|
413
|
+
f"(id: {user_msg_id}) {user_snippet}",
|
|
414
|
+
data={
|
|
415
|
+
"index": user_msg_ordinal,
|
|
416
|
+
"id": user_msg_id,
|
|
417
|
+
"total": total_user_msgs,
|
|
418
|
+
"snippet": user_snippet,
|
|
419
|
+
},
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
s_timeline = report.section(f"{turn_label}Trace · 时间轴")
|
|
423
|
+
_section_timeline(s_timeline, analysis)
|
|
424
|
+
|
|
425
|
+
s_summary = report.section(f"{turn_label}Trace · 汇总")
|
|
426
|
+
_section_summary(s_summary, analysis)
|
|
427
|
+
|
|
428
|
+
if analysis["model_calls"]:
|
|
429
|
+
s_models = report.section(f"{turn_label}Trace · Model 拆解")
|
|
430
|
+
_section_model_breakdown(s_models, analysis)
|
|
431
|
+
|
|
432
|
+
if analysis["tool_execs"]:
|
|
433
|
+
s_tools = report.section(f"{turn_label}Trace · 工具拆解")
|
|
434
|
+
_section_tool_breakdown(s_tools, analysis)
|
|
435
|
+
|
|
436
|
+
# Slow E2E → WARN. Attach to this turn's summary section so the verdict
|
|
437
|
+
# surfaces without inventing a new section.
|
|
438
|
+
total_ms = analysis["summary"]["total_ms"]
|
|
439
|
+
if total_ms > SLOW_THRESHOLD_MS:
|
|
440
|
+
s_summary.warn(
|
|
441
|
+
"trace.slow",
|
|
442
|
+
f"E2E elapsed {fmt_duration(total_ms)} > "
|
|
443
|
+
f"{SLOW_THRESHOLD_MS // 1000}s",
|
|
444
|
+
data={"total_ms": total_ms, "threshold_ms": SLOW_THRESHOLD_MS},
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def _section_aggregate(
|
|
449
|
+
s: Section, per_turn: List[Dict[str, Any]],
|
|
450
|
+
) -> None:
|
|
451
|
+
"""Render the cross-turn summary at the end of an --all-messages run.
|
|
452
|
+
|
|
453
|
+
Aggregates totals across every turn (count, cumulative E2E / model /
|
|
454
|
+
tool time, cumulative tokens) and surfaces any turn whose E2E exceeded
|
|
455
|
+
SLOW_THRESHOLD_MS as a per-turn WARN line.
|
|
456
|
+
"""
|
|
457
|
+
n = len(per_turn)
|
|
458
|
+
total_e2e = sum(t["summary"]["total_ms"] for t in per_turn)
|
|
459
|
+
total_model = sum(t["summary"]["model_total_ms"] for t in per_turn)
|
|
460
|
+
total_tool = sum(t["summary"]["tool_total_ms"] for t in per_turn)
|
|
461
|
+
total_in = sum(t["summary"]["total_input_tokens"] for t in per_turn)
|
|
462
|
+
total_out = sum(t["summary"]["total_output_tokens"] for t in per_turn)
|
|
463
|
+
total_cr = sum(t["summary"]["total_cache_read"] for t in per_turn)
|
|
464
|
+
total_cw = sum(t["summary"]["total_cache_write"] for t in per_turn)
|
|
465
|
+
total_models = sum(t["summary"]["model_count"] for t in per_turn)
|
|
466
|
+
total_tools = sum(t["summary"]["tool_count"] for t in per_turn)
|
|
467
|
+
|
|
468
|
+
s.ok(
|
|
469
|
+
"agg.turns", f"Turns: {n}",
|
|
470
|
+
data={"count": n},
|
|
471
|
+
)
|
|
472
|
+
s.ok(
|
|
473
|
+
"agg.total", f"Cumulative E2E: {fmt_duration(total_e2e)}",
|
|
474
|
+
data={"total_ms": total_e2e},
|
|
475
|
+
)
|
|
476
|
+
s.ok(
|
|
477
|
+
"agg.model",
|
|
478
|
+
f"Cumulative model time: {fmt_duration(total_model)} "
|
|
479
|
+
f"across {total_models} call(s)",
|
|
480
|
+
data={"total_ms": total_model, "count": total_models},
|
|
481
|
+
)
|
|
482
|
+
s.ok(
|
|
483
|
+
"agg.tool",
|
|
484
|
+
f"Cumulative tool time: {fmt_duration(total_tool)} "
|
|
485
|
+
f"across {total_tools} call(s)",
|
|
486
|
+
data={"total_ms": total_tool, "count": total_tools},
|
|
487
|
+
)
|
|
488
|
+
tok_msg = f"Cumulative tokens: in={total_in} out={total_out}"
|
|
489
|
+
if total_cr:
|
|
490
|
+
tok_msg += f" cache_read={total_cr}"
|
|
491
|
+
if total_cw:
|
|
492
|
+
tok_msg += f" cache_write={total_cw}"
|
|
493
|
+
s.ok(
|
|
494
|
+
"agg.tokens", tok_msg,
|
|
495
|
+
data={
|
|
496
|
+
"input": total_in,
|
|
497
|
+
"output": total_out,
|
|
498
|
+
"cache_read": total_cr,
|
|
499
|
+
"cache_write": total_cw,
|
|
500
|
+
},
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
for t in per_turn:
|
|
504
|
+
if t["summary"]["total_ms"] > SLOW_THRESHOLD_MS:
|
|
505
|
+
s.warn(
|
|
506
|
+
f"agg.slow.{t['index']}",
|
|
507
|
+
f"Turn #{t['index'] + 1} slow: "
|
|
508
|
+
f"{fmt_duration(t['summary']['total_ms'])} > "
|
|
509
|
+
f"{SLOW_THRESHOLD_MS // 1000}s "
|
|
510
|
+
f"(id: {t['user_message_id']})",
|
|
511
|
+
data={
|
|
512
|
+
"index": t["index"],
|
|
513
|
+
"user_message_id": t["user_message_id"],
|
|
514
|
+
"total_ms": t["summary"]["total_ms"],
|
|
515
|
+
"threshold_ms": SLOW_THRESHOLD_MS,
|
|
516
|
+
},
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
|
|
373
520
|
@register
|
|
374
521
|
class TraceInspector:
|
|
375
522
|
id = "trace"
|
|
@@ -402,6 +549,28 @@ class TraceInspector:
|
|
|
402
549
|
report.elapsed_ms = (time.time() - t0) * 1000
|
|
403
550
|
return report
|
|
404
551
|
|
|
552
|
+
# --all-messages is mutually exclusive with the single-turn selectors.
|
|
553
|
+
# The CLI parser already rejects the combination, but inspectors can
|
|
554
|
+
# also be called programmatically (tests, future SDK callers); guard
|
|
555
|
+
# here so the contract holds end-to-end.
|
|
556
|
+
all_messages = bool(kwargs.get("all_messages"))
|
|
557
|
+
if all_messages and (
|
|
558
|
+
kwargs.get("msg_index") is not None
|
|
559
|
+
or kwargs.get("msg_id") is not None
|
|
560
|
+
or kwargs.get("msg_match") is not None
|
|
561
|
+
):
|
|
562
|
+
report.error = (
|
|
563
|
+
"--all-messages cannot be combined with "
|
|
564
|
+
"--msg-index/--msg-id/--msg-match"
|
|
565
|
+
)
|
|
566
|
+
report.diag_error = DiagError(
|
|
567
|
+
code="INVALID_ARGUMENT",
|
|
568
|
+
message=report.error,
|
|
569
|
+
hint="pick either --all-messages OR a single-turn selector",
|
|
570
|
+
)
|
|
571
|
+
report.elapsed_ms = (time.time() - t0) * 1000
|
|
572
|
+
return report
|
|
573
|
+
|
|
405
574
|
files, candidates = sessions.resolve(
|
|
406
575
|
session_id,
|
|
407
576
|
base_dir=str(ctx.sessions_base),
|
|
@@ -466,41 +635,69 @@ class TraceInspector:
|
|
|
466
635
|
report.elapsed_ms = (time.time() - t0) * 1000
|
|
467
636
|
return report
|
|
468
637
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
)
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
638
|
+
# ── Pick which user turn(s) to analyze ──
|
|
639
|
+
# Single-turn (default, or any --msg-* selector) → one analysis.
|
|
640
|
+
# All-messages → analyze every user turn from find_user_messages.
|
|
641
|
+
# We still call analyze_phases per turn so each block has its own
|
|
642
|
+
# base_epoch_ms / timeline / summary identical to a single-turn run.
|
|
643
|
+
total_user_msgs = len(user_msgs)
|
|
644
|
+
if all_messages:
|
|
645
|
+
selected: List[tuple] = list(user_msgs)
|
|
646
|
+
else:
|
|
647
|
+
rec_idx, user_rec = select_user_message(
|
|
648
|
+
records,
|
|
649
|
+
kwargs.get("msg_index"),
|
|
650
|
+
kwargs.get("msg_id"),
|
|
651
|
+
kwargs.get("msg_match"),
|
|
478
652
|
)
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
653
|
+
selected = [(rec_idx, user_rec)]
|
|
654
|
+
|
|
655
|
+
# Run analyze_phases for each selected turn. The first turn's
|
|
656
|
+
# analysis seeds the session-level enrichment (trajectory / system
|
|
657
|
+
# prompt / gateway) — those data sources are session-scoped, not
|
|
658
|
+
# per-turn, so they are computed ONCE using the first turn's
|
|
659
|
+
# base_epoch_ms (which is the earliest timestamp in selected turns).
|
|
660
|
+
per_turn_analyses: List[Dict[str, Any]] = []
|
|
661
|
+
for ordinal, (rec_idx, user_rec) in enumerate(selected):
|
|
662
|
+
try:
|
|
663
|
+
turn_index = next(
|
|
664
|
+
i for i, (ri, _) in enumerate(user_msgs) if ri == rec_idx
|
|
665
|
+
)
|
|
666
|
+
except StopIteration:
|
|
667
|
+
turn_index = ordinal
|
|
668
|
+
turn_records = extract_trace_records(records, rec_idx)
|
|
669
|
+
analysis = analyze_phases(turn_records)
|
|
670
|
+
per_turn_analyses.append({
|
|
671
|
+
"index": turn_index,
|
|
672
|
+
"user_message_id": user_rec.get("id", "?"),
|
|
673
|
+
"user_record": user_rec,
|
|
674
|
+
"analysis": analysis,
|
|
675
|
+
})
|
|
676
|
+
|
|
677
|
+
# ── Session-level enrichment (computed once) ──
|
|
678
|
+
# Trajectory / system prompt / gateway timing reflect properties of
|
|
679
|
+
# the whole run/file, not a single user turn. Computing them once
|
|
680
|
+
# keeps cost down on long sessions and avoids duplicating identical
|
|
681
|
+
# blocks per turn. The base epoch we hand to load_trajectory_info /
|
|
682
|
+
# load_gateway_timing is the FIRST selected turn's base_epoch_ms;
|
|
683
|
+
# for single-turn this matches the legacy behaviour exactly, and for
|
|
684
|
+
# all-messages it puts the trajectory offsets on the same axis as
|
|
685
|
+
# the first turn block (the most useful anchor point).
|
|
686
|
+
first_analysis = per_turn_analyses[0]["analysis"]
|
|
687
|
+
base_epoch_ms = first_analysis["base_epoch_ms"]
|
|
492
688
|
|
|
493
689
|
traj_info: Optional[Dict[str, Any]] = None
|
|
494
690
|
if not kwargs.get("no_trajectory"):
|
|
495
691
|
traj_path = find_trajectory_file(session_file)
|
|
496
692
|
if traj_path:
|
|
497
|
-
traj_info = load_trajectory_info(traj_path,
|
|
693
|
+
traj_info = load_trajectory_info(traj_path, base_epoch_ms)
|
|
498
694
|
if traj_info is not None:
|
|
499
695
|
_apply_traj_redaction(
|
|
500
696
|
traj_info,
|
|
501
697
|
mask=bool(kwargs.get("mask")),
|
|
502
698
|
show_tool_metas=bool(kwargs.get("show_tool_metas")),
|
|
503
|
-
show_plugin_snapshot=bool(
|
|
699
|
+
show_plugin_snapshot=bool(
|
|
700
|
+
kwargs.get("show_plugin_snapshot")),
|
|
504
701
|
)
|
|
505
702
|
|
|
506
703
|
gw_info: Optional[Dict[str, Any]] = None
|
|
@@ -508,15 +705,15 @@ class TraceInspector:
|
|
|
508
705
|
log_files = find_gateway_logs(str(ctx.log_dir))
|
|
509
706
|
if log_files:
|
|
510
707
|
gw_info = load_gateway_timing(
|
|
511
|
-
log_files, full_session_id,
|
|
708
|
+
log_files, full_session_id, base_epoch_ms,
|
|
512
709
|
)
|
|
513
710
|
|
|
514
711
|
store_report = sessions.lookup_system_prompt_report(
|
|
515
712
|
session_file, full_session_id,
|
|
516
713
|
)
|
|
517
714
|
system_prompt = build_system_prompt_info(store_report, traj_info)
|
|
518
|
-
if system_prompt is not None and
|
|
519
|
-
first_call =
|
|
715
|
+
if system_prompt is not None and first_analysis.get("model_calls"):
|
|
716
|
+
first_call = first_analysis["model_calls"][0]
|
|
520
717
|
actual_input = (
|
|
521
718
|
(first_call.get("tokens_in") or 0)
|
|
522
719
|
+ (first_call.get("cache_read") or 0)
|
|
@@ -532,58 +729,179 @@ class TraceInspector:
|
|
|
532
729
|
if system_prompt is not None:
|
|
533
730
|
report.data["systemPrompt"] = system_prompt
|
|
534
731
|
|
|
535
|
-
# ──
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
)
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
"
|
|
549
|
-
data
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
732
|
+
# ── report.data wiring ──
|
|
733
|
+
# Single-turn path: keep the legacy keys (timeline, model_calls,
|
|
734
|
+
# tool_execs, summary, base_epoch_ms, user_message_*) at the top
|
|
735
|
+
# level so existing JSON consumers see no schema change.
|
|
736
|
+
# All-messages path: add report.data["all_messages"] (per-turn list)
|
|
737
|
+
# and report.data["aggregate"] (cumulative). The legacy single-turn
|
|
738
|
+
# keys are NOT populated in this mode — consumers that asked for
|
|
739
|
+
# all-messages should read the new keys.
|
|
740
|
+
if not all_messages:
|
|
741
|
+
single = per_turn_analyses[0]
|
|
742
|
+
report.data["base_epoch_ms"] = single["analysis"]["base_epoch_ms"]
|
|
743
|
+
report.data["timeline"] = single["analysis"]["events"]
|
|
744
|
+
report.data["model_calls"] = single["analysis"]["model_calls"]
|
|
745
|
+
report.data["tool_execs"] = single["analysis"]["tool_execs"]
|
|
746
|
+
report.data["summary"] = single["analysis"]["summary"]
|
|
747
|
+
report.data["user_message_index"] = single["index"]
|
|
748
|
+
report.data["user_message_id"] = single["user_message_id"]
|
|
749
|
+
else:
|
|
750
|
+
all_msgs_payload: List[Dict[str, Any]] = []
|
|
751
|
+
for t in per_turn_analyses:
|
|
752
|
+
a = t["analysis"]
|
|
753
|
+
all_msgs_payload.append({
|
|
754
|
+
"index": t["index"],
|
|
755
|
+
"user_message_id": t["user_message_id"],
|
|
756
|
+
"user_message_snippet": _user_text_snippet(t["user_record"]),
|
|
757
|
+
"base_epoch_ms": a["base_epoch_ms"],
|
|
758
|
+
"timeline": a["events"],
|
|
759
|
+
"model_calls": a["model_calls"],
|
|
760
|
+
"tool_execs": a["tool_execs"],
|
|
761
|
+
"summary": a["summary"],
|
|
762
|
+
})
|
|
763
|
+
report.data["all_messages"] = all_msgs_payload
|
|
764
|
+
report.data["aggregate"] = {
|
|
765
|
+
"turns": len(per_turn_analyses),
|
|
766
|
+
"total_ms": sum(
|
|
767
|
+
t["analysis"]["summary"]["total_ms"]
|
|
768
|
+
for t in per_turn_analyses
|
|
769
|
+
),
|
|
770
|
+
"model_total_ms": sum(
|
|
771
|
+
t["analysis"]["summary"]["model_total_ms"]
|
|
772
|
+
for t in per_turn_analyses
|
|
773
|
+
),
|
|
774
|
+
"tool_total_ms": sum(
|
|
775
|
+
t["analysis"]["summary"]["tool_total_ms"]
|
|
776
|
+
for t in per_turn_analyses
|
|
777
|
+
),
|
|
778
|
+
"total_input_tokens": sum(
|
|
779
|
+
t["analysis"]["summary"]["total_input_tokens"]
|
|
780
|
+
for t in per_turn_analyses
|
|
781
|
+
),
|
|
782
|
+
"total_output_tokens": sum(
|
|
783
|
+
t["analysis"]["summary"]["total_output_tokens"]
|
|
784
|
+
for t in per_turn_analyses
|
|
785
|
+
),
|
|
786
|
+
"total_cache_read": sum(
|
|
787
|
+
t["analysis"]["summary"]["total_cache_read"]
|
|
788
|
+
for t in per_turn_analyses
|
|
789
|
+
),
|
|
790
|
+
"total_cache_write": sum(
|
|
791
|
+
t["analysis"]["summary"]["total_cache_write"]
|
|
792
|
+
for t in per_turn_analyses
|
|
793
|
+
),
|
|
794
|
+
}
|
|
569
795
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
796
|
+
# ── Sections ──
|
|
797
|
+
if not all_messages:
|
|
798
|
+
# Single-turn: emit one "session" line under 元信息 followed by
|
|
799
|
+
# the per-turn block, byte-identical to pre-1.11.0 output.
|
|
800
|
+
single = per_turn_analyses[0]
|
|
801
|
+
s_meta = report.section("Trace · 元信息")
|
|
802
|
+
s_meta.ok(
|
|
803
|
+
"session", f"session: {full_session_id}",
|
|
804
|
+
data={"session_id": full_session_id, "file": session_file},
|
|
805
|
+
)
|
|
806
|
+
msg_hint = (
|
|
807
|
+
f"user message #{single['index']} of {total_user_msgs} "
|
|
808
|
+
f"(id: {single['user_message_id']})"
|
|
809
|
+
)
|
|
810
|
+
if total_user_msgs > 1:
|
|
811
|
+
msg_hint += (
|
|
812
|
+
f" [use --msg-index 0~{total_user_msgs - 1} to select]"
|
|
813
|
+
)
|
|
814
|
+
s_meta.ok(
|
|
815
|
+
"user_message", msg_hint,
|
|
816
|
+
data={
|
|
817
|
+
"index": single["index"],
|
|
818
|
+
"id": single["user_message_id"],
|
|
819
|
+
"total": total_user_msgs,
|
|
820
|
+
},
|
|
821
|
+
)
|
|
822
|
+
s_timeline = report.section("Trace · 时间轴")
|
|
823
|
+
_section_timeline(s_timeline, single["analysis"])
|
|
824
|
+
s_summary = report.section("Trace · 汇总")
|
|
825
|
+
_section_summary(s_summary, single["analysis"])
|
|
826
|
+
if single["analysis"]["model_calls"]:
|
|
827
|
+
s_models = report.section("Trace · Model 拆解")
|
|
828
|
+
_section_model_breakdown(s_models, single["analysis"])
|
|
829
|
+
if single["analysis"]["tool_execs"]:
|
|
830
|
+
s_tools = report.section("Trace · 工具拆解")
|
|
831
|
+
_section_tool_breakdown(s_tools, single["analysis"])
|
|
832
|
+
|
|
833
|
+
if traj_info:
|
|
834
|
+
s_traj = report.section("Trace · Trajectory")
|
|
835
|
+
_section_trajectory(s_traj, traj_info)
|
|
836
|
+
if system_prompt:
|
|
837
|
+
s_sp = report.section("Trace · System Prompt")
|
|
838
|
+
_section_system_prompt(s_sp, system_prompt)
|
|
839
|
+
if gw_info:
|
|
840
|
+
s_gw = report.section("Trace · Gateway 计时")
|
|
841
|
+
_section_gateway(s_gw, gw_info)
|
|
842
|
+
|
|
843
|
+
total_ms = single["analysis"]["summary"]["total_ms"]
|
|
844
|
+
if total_ms > SLOW_THRESHOLD_MS:
|
|
845
|
+
s_summary.warn(
|
|
846
|
+
"trace.slow",
|
|
847
|
+
f"E2E elapsed {fmt_duration(total_ms)} > "
|
|
848
|
+
f"{SLOW_THRESHOLD_MS // 1000}s",
|
|
849
|
+
data={
|
|
850
|
+
"total_ms": total_ms,
|
|
851
|
+
"threshold_ms": SLOW_THRESHOLD_MS,
|
|
852
|
+
},
|
|
853
|
+
)
|
|
854
|
+
else:
|
|
855
|
+
# All-messages: a session-wide overview, then per-turn blocks
|
|
856
|
+
# delimited by a "Turn #i/N" prefix, then session-level
|
|
857
|
+
# enrichment ONCE, then a final aggregate section.
|
|
858
|
+
n = len(per_turn_analyses)
|
|
859
|
+
s_overview = report.section("Trace · 元信息 (全部消息)")
|
|
860
|
+
s_overview.ok(
|
|
861
|
+
"session", f"session: {full_session_id}",
|
|
862
|
+
data={"session_id": full_session_id, "file": session_file},
|
|
863
|
+
)
|
|
864
|
+
s_overview.ok(
|
|
865
|
+
"all_messages",
|
|
866
|
+
f"tracing all {n} user message(s) in this session",
|
|
867
|
+
data={"count": n},
|
|
868
|
+
)
|
|
573
869
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
870
|
+
for t in per_turn_analyses:
|
|
871
|
+
turn_label = f"Turn #{t['index'] + 1}/{total_user_msgs} · "
|
|
872
|
+
_build_turn_sections(
|
|
873
|
+
report,
|
|
874
|
+
analysis=t["analysis"],
|
|
875
|
+
user_msg_ordinal=t["index"],
|
|
876
|
+
user_msg_id=t["user_message_id"],
|
|
877
|
+
total_user_msgs=total_user_msgs,
|
|
878
|
+
turn_label=turn_label,
|
|
879
|
+
user_snippet=_user_text_snippet(t["user_record"]),
|
|
880
|
+
)
|
|
577
881
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
882
|
+
# Session-scoped enrichment is rendered ONCE — emitting it per
|
|
883
|
+
# turn would duplicate large, identical blocks (system prompt
|
|
884
|
+
# alone can be 10k+ chars). Title-prefixed with "Session · " so
|
|
885
|
+
# the reader sees it is run-wide, not turn-specific.
|
|
886
|
+
if traj_info:
|
|
887
|
+
s_traj = report.section("Session · Trajectory")
|
|
888
|
+
_section_trajectory(s_traj, traj_info)
|
|
889
|
+
if system_prompt:
|
|
890
|
+
s_sp = report.section("Session · System Prompt")
|
|
891
|
+
_section_system_prompt(s_sp, system_prompt)
|
|
892
|
+
if gw_info:
|
|
893
|
+
s_gw = report.section("Session · Gateway 计时")
|
|
894
|
+
_section_gateway(s_gw, gw_info)
|
|
895
|
+
|
|
896
|
+
s_agg = report.section("Trace · 跨消息汇总")
|
|
897
|
+
_section_aggregate(s_agg, [
|
|
898
|
+
{
|
|
899
|
+
"index": t["index"],
|
|
900
|
+
"user_message_id": t["user_message_id"],
|
|
901
|
+
"summary": t["analysis"]["summary"],
|
|
902
|
+
}
|
|
903
|
+
for t in per_turn_analyses
|
|
904
|
+
])
|
|
587
905
|
|
|
588
906
|
report.elapsed_ms = (time.time() - t0) * 1000
|
|
589
907
|
return report
|
package/ocdiag/main.py
CHANGED
|
@@ -36,6 +36,37 @@ from .render.ndjson import NdjsonRenderer
|
|
|
36
36
|
_FORMAT_CHOICES = ("pretty", "json", "ndjson")
|
|
37
37
|
|
|
38
38
|
|
|
39
|
+
# Centralized command description table (id -> one-line English summary).
|
|
40
|
+
# Reused by `list`, `<id> --help`, and the top-level help. When adding a new
|
|
41
|
+
# collector/inspector, add a row here; missing ids fall back to "" via _desc().
|
|
42
|
+
_COMMAND_DESC = {
|
|
43
|
+
# Scan-type collectors (state)
|
|
44
|
+
"channel": "Scan channel (Feishu/Telegram/etc.) connect + message logs; flag disconnects, expired-discard, auth failures",
|
|
45
|
+
"configuration": "Parse key openclaw.json settings (agents/models/plugins/channels) and flag missing or risky values",
|
|
46
|
+
"cron_jobs": "List every cron job's full config (schedule/payload/sessionTarget/delivery/enabled); detect no-fire / delivery failures",
|
|
47
|
+
"doctor": "Check Node / Python / openclaw-diag / OpenClaw install and versions are ready",
|
|
48
|
+
"environment": "Collect host environment, Gateway process, and OpenClaw version basics",
|
|
49
|
+
"gateway": "Analyze Gateway process lifecycle logs (start/restart/WS connect/crash)",
|
|
50
|
+
"performance": "Measure model-call latency (E2E/TTFT), tool durations, and availability",
|
|
51
|
+
"plugin_diag": "Check plugin load status, hook subscriptions, trust gate, and plugin errors",
|
|
52
|
+
"recent_errors": "Extract and categorize recent errors/exceptions from logs",
|
|
53
|
+
"run_health": "Assess agent run completion rate, stalls, and interruptions",
|
|
54
|
+
"sessions_diag": "Scan session.jsonl; count toolCall/toolResult pairing, orphans, anomalies",
|
|
55
|
+
"shell_history": "Summarize shell commands the agent has executed",
|
|
56
|
+
"sys_health": "Check system-level health: CPU/memory/disk/OOM/processes",
|
|
57
|
+
"task_health": "Assess background task (openclaw tasks) status and health",
|
|
58
|
+
# Object-type inspectors
|
|
59
|
+
"extract": "Export a session file to readable form (incl. active/reset/deleted/backup versions)",
|
|
60
|
+
"panorama": "360° diagnosis: correlate trajectory + app logs + subtasks + cron",
|
|
61
|
+
"trace": "Trace one user message's full lifecycle (prompt->toolCall->toolResult->reply)",
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _desc(mid: str) -> str:
|
|
66
|
+
"""Return the one-line English description for a command id, or ""."""
|
|
67
|
+
return _COMMAND_DESC.get(mid, "")
|
|
68
|
+
|
|
69
|
+
|
|
39
70
|
def _paged_print(text: str) -> None:
|
|
40
71
|
"""Print text through a pager when stdout is a TTY and output is long.
|
|
41
72
|
|
|
@@ -99,28 +130,53 @@ def _build_context(args) -> DiagContext:
|
|
|
99
130
|
|
|
100
131
|
|
|
101
132
|
def _common_arguments(p: argparse.ArgumentParser) -> None:
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
p.
|
|
106
|
-
|
|
133
|
+
"""Register the global options shared by every subcommand, in a dedicated
|
|
134
|
+
argument group so --help output stays aligned.
|
|
135
|
+
"""
|
|
136
|
+
g = p.add_argument_group("global options")
|
|
137
|
+
g.add_argument(
|
|
138
|
+
"--config", metavar="PATH", default=paths.CONFIG,
|
|
139
|
+
help="Path to openclaw.json (default: ~/.openclaw/openclaw.json)",
|
|
140
|
+
)
|
|
141
|
+
g.add_argument(
|
|
142
|
+
"--log-dir", metavar="PATH", default=paths.LOG_DIR,
|
|
143
|
+
help="OpenClaw log directory (default: /tmp/openclaw)",
|
|
144
|
+
)
|
|
145
|
+
g.add_argument(
|
|
146
|
+
"--sessions-base", metavar="PATH", default=paths.SESSIONS_BASE,
|
|
147
|
+
help="Sessions root directory (default: ~/.openclaw/agents)",
|
|
148
|
+
)
|
|
149
|
+
g.add_argument(
|
|
150
|
+
"--openclaw-home", metavar="PATH", default=paths.OPENCLAW_HOME,
|
|
151
|
+
help="OpenClaw home directory (default: ~/.openclaw)",
|
|
152
|
+
)
|
|
153
|
+
g.add_argument(
|
|
107
154
|
"--format",
|
|
108
155
|
choices=list(_FORMAT_CHOICES),
|
|
109
156
|
default=None,
|
|
110
|
-
help="Output format
|
|
157
|
+
help="Output format pretty|json|ndjson (default: pretty; json/ndjson suit agents/scripts)",
|
|
158
|
+
)
|
|
159
|
+
g.add_argument(
|
|
160
|
+
"--json", action="store_true",
|
|
161
|
+
help="Alias for --format json",
|
|
162
|
+
)
|
|
163
|
+
g.add_argument(
|
|
164
|
+
"--no-color", action="store_true",
|
|
165
|
+
help="Disable ANSI color (use when writing to a file or pipe)",
|
|
166
|
+
)
|
|
167
|
+
g.add_argument(
|
|
168
|
+
"--unmask", action="store_true",
|
|
169
|
+
help="Do not mask; show raw sensitive content (tokens/message bodies). Masked by default",
|
|
111
170
|
)
|
|
112
|
-
p.add_argument("--json", action="store_true", help="Alias for --format json.")
|
|
113
|
-
p.add_argument("--no-color", action="store_true")
|
|
114
|
-
p.add_argument("--unmask", action="store_true")
|
|
115
171
|
|
|
116
172
|
|
|
117
173
|
def _channel_arguments(p: argparse.ArgumentParser) -> None:
|
|
118
|
-
p.
|
|
174
|
+
g = p.add_argument_group("channel options")
|
|
175
|
+
g.add_argument(
|
|
119
176
|
"--account", default=None,
|
|
120
177
|
help="Filter channel signals by account substring "
|
|
121
178
|
"(matched against the channel-prefix portion of the message body, "
|
|
122
|
-
"e.g.
|
|
123
|
-
"Default: no filter.",
|
|
179
|
+
"e.g. --account default keeps only feishu[default]: lines). Default: no filter",
|
|
124
180
|
)
|
|
125
181
|
|
|
126
182
|
|
|
@@ -159,63 +215,67 @@ def cmd_list(args) -> int:
|
|
|
159
215
|
if fmt != "pretty":
|
|
160
216
|
payload = {
|
|
161
217
|
"state_collectors": [
|
|
162
|
-
{"id": c.id, "label": c.
|
|
218
|
+
{"id": c.id, "label": _desc(c.id), "description": _desc(c.id)}
|
|
219
|
+
for c in state
|
|
163
220
|
],
|
|
164
221
|
"object_inspectors": [
|
|
165
|
-
{"id": c.id, "label": c.
|
|
222
|
+
{"id": c.id, "label": _desc(c.id), "description": _desc(c.id)}
|
|
223
|
+
for c in inspectors
|
|
166
224
|
],
|
|
167
225
|
}
|
|
168
226
|
print(json.dumps(payload, ensure_ascii=False))
|
|
169
227
|
return 0
|
|
170
|
-
print("openclaw-diag —
|
|
228
|
+
print("openclaw-diag — available diagnostics (v2)")
|
|
171
229
|
print()
|
|
172
|
-
print("
|
|
230
|
+
print(" Scan type (no args required):")
|
|
173
231
|
for c in state:
|
|
174
|
-
|
|
232
|
+
d = _desc(c.id)
|
|
233
|
+
print(f" {c.id:<16s} {d}")
|
|
175
234
|
print()
|
|
176
235
|
if inspectors:
|
|
177
|
-
print("
|
|
236
|
+
print(" Object type (require session uuid):")
|
|
178
237
|
for c in inspectors:
|
|
179
|
-
|
|
238
|
+
d = _desc(c.id)
|
|
239
|
+
print(f" {c.id:<16s} {d}")
|
|
180
240
|
print()
|
|
181
|
-
print("
|
|
182
|
-
print(" all
|
|
183
|
-
print(" doctor
|
|
184
|
-
print(" examples
|
|
241
|
+
print(" Other commands:")
|
|
242
|
+
print(" all Run all scan-type diagnostics at once")
|
|
243
|
+
print(" doctor Check Node / Python / openclaw-diag / OpenClaw environment")
|
|
244
|
+
print(" examples Print common usage examples")
|
|
185
245
|
return 0
|
|
186
246
|
|
|
187
247
|
|
|
188
248
|
def cmd_examples() -> int:
|
|
189
|
-
print("""openclaw-diag —
|
|
249
|
+
print("""openclaw-diag — common scenarios
|
|
190
250
|
|
|
191
|
-
#
|
|
251
|
+
# Full health check
|
|
192
252
|
openclaw-diag all
|
|
193
253
|
|
|
194
|
-
# JSON
|
|
254
|
+
# JSON output (for agents / scripts)
|
|
195
255
|
openclaw-diag all --format json
|
|
196
256
|
|
|
197
|
-
#
|
|
257
|
+
# Check Gateway status
|
|
198
258
|
openclaw-diag gateway
|
|
199
259
|
|
|
200
|
-
#
|
|
260
|
+
# Trace one message's full lifecycle
|
|
201
261
|
openclaw-diag trace <uuid>
|
|
202
262
|
openclaw-diag trace abc12345 --msg-index 0
|
|
203
263
|
|
|
204
|
-
#
|
|
264
|
+
# Export session conversation content
|
|
205
265
|
openclaw-diag extract <uuid>
|
|
206
266
|
openclaw-diag extract abc12345 --summary
|
|
207
267
|
|
|
208
|
-
#
|
|
268
|
+
# Session panorama diagnosis (correlates trajectory + logs + subtasks + cron)
|
|
209
269
|
openclaw-diag panorama <uuid>
|
|
210
270
|
openclaw-diag panorama abc12345 --strict-correlation --format json
|
|
211
271
|
|
|
212
|
-
#
|
|
272
|
+
# Model performance
|
|
213
273
|
openclaw-diag performance
|
|
214
274
|
|
|
215
|
-
#
|
|
275
|
+
# Cron job status
|
|
216
276
|
openclaw-diag cron_jobs
|
|
217
277
|
|
|
218
|
-
#
|
|
278
|
+
# Quick verdict via jq
|
|
219
279
|
openclaw-diag all --format json | jq '.data.verdict'
|
|
220
280
|
""")
|
|
221
281
|
return 0
|
|
@@ -300,7 +360,7 @@ def cmd_all(args, skip_ids: List[str]) -> int:
|
|
|
300
360
|
def cmd_run_collector(args, mid: str) -> int:
|
|
301
361
|
c = registry.get(mid)
|
|
302
362
|
if c is None:
|
|
303
|
-
print(f"Error:
|
|
363
|
+
print(f"Error: unknown collector '{mid}'", file=sys.stderr)
|
|
304
364
|
return EXIT_INPUT_ERROR
|
|
305
365
|
ctx = _build_context(args)
|
|
306
366
|
t0 = time.time()
|
|
@@ -319,21 +379,22 @@ def cmd_run_collector(args, mid: str) -> int:
|
|
|
319
379
|
return _exit_code(report)
|
|
320
380
|
|
|
321
381
|
|
|
322
|
-
_TRACE_EPILOG = """
|
|
323
|
-
openclaw-diag trace 7e9f3b31 #
|
|
324
|
-
openclaw-diag trace 7e9f3b31 --msg-index 0 #
|
|
325
|
-
openclaw-diag trace 7e9f3b31 --msg-match deploy #
|
|
326
|
-
openclaw-diag trace 7e9f3b31 --
|
|
382
|
+
_TRACE_EPILOG = """Examples:
|
|
383
|
+
openclaw-diag trace 7e9f3b31 # last user message in the session
|
|
384
|
+
openclaw-diag trace 7e9f3b31 --msg-index 0 # the first one
|
|
385
|
+
openclaw-diag trace 7e9f3b31 --msg-match deploy # match by content
|
|
386
|
+
openclaw-diag trace 7e9f3b31 --all-messages # trace every user message in one run (one block per turn)
|
|
387
|
+
openclaw-diag trace 7e9f3b31 -A --format json # all user messages, JSON output
|
|
327
388
|
"""
|
|
328
389
|
|
|
329
|
-
_EXTRACT_EPILOG = """
|
|
330
|
-
openclaw-diag extract 7e9f3b31 #
|
|
331
|
-
openclaw-diag extract 7e9f3b31 --summary #
|
|
332
|
-
openclaw-diag extract 7e9f3b31 --all #
|
|
390
|
+
_EXTRACT_EPILOG = """Examples:
|
|
391
|
+
openclaw-diag extract 7e9f3b31 # export the active file by default
|
|
392
|
+
openclaw-diag extract 7e9f3b31 --summary # stats only
|
|
393
|
+
openclaw-diag extract 7e9f3b31 --all # include reset / deleted / backup
|
|
333
394
|
openclaw-diag extract 7e9f3b31 --format json
|
|
334
395
|
"""
|
|
335
396
|
|
|
336
|
-
_PANORAMA_EPILOG = """
|
|
397
|
+
_PANORAMA_EPILOG = """Examples:
|
|
337
398
|
openclaw-diag panorama 7e9f3b31 # latest run
|
|
338
399
|
openclaw-diag panorama 7e9f3b31 --all-runs # every run
|
|
339
400
|
openclaw-diag panorama 7e9f3b31 --run-index 0 # first run
|
|
@@ -343,67 +404,139 @@ _PANORAMA_EPILOG = """示例:
|
|
|
343
404
|
|
|
344
405
|
|
|
345
406
|
def _build_trace_parser() -> argparse.ArgumentParser:
|
|
407
|
+
desc_text = _desc("trace")
|
|
408
|
+
description = f"{desc_text} (trace)" if desc_text else "(trace)"
|
|
346
409
|
p = argparse.ArgumentParser(
|
|
347
410
|
prog="openclaw-diag trace",
|
|
411
|
+
description=description,
|
|
348
412
|
add_help=True,
|
|
349
413
|
epilog=_TRACE_EPILOG,
|
|
350
414
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
351
415
|
)
|
|
352
|
-
p.
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
416
|
+
g = p.add_argument_group("trace options")
|
|
417
|
+
g.add_argument(
|
|
418
|
+
"session_id",
|
|
419
|
+
help="Target session UUID (full or 8+ char prefix)",
|
|
420
|
+
)
|
|
421
|
+
g.add_argument(
|
|
422
|
+
"--msg-index", type=int, default=None,
|
|
423
|
+
help="Nth user message (0-based)",
|
|
424
|
+
)
|
|
425
|
+
g.add_argument(
|
|
426
|
+
"--msg-id", default=None,
|
|
427
|
+
help="User message by id field",
|
|
428
|
+
)
|
|
429
|
+
g.add_argument(
|
|
430
|
+
"--msg-match", default=None,
|
|
431
|
+
help="First user message containing TEXT",
|
|
432
|
+
)
|
|
433
|
+
g.add_argument(
|
|
434
|
+
"-A", "--all-messages", action="store_true", dest="all_messages",
|
|
435
|
+
help="Trace every user message in the session "
|
|
436
|
+
"(mutually exclusive with --msg-index/--msg-id/--msg-match)",
|
|
437
|
+
)
|
|
438
|
+
g.add_argument(
|
|
439
|
+
"--no-trajectory", action="store_true",
|
|
440
|
+
help="Do not read trajectory.jsonl; analyze from session.jsonl only",
|
|
441
|
+
)
|
|
442
|
+
g.add_argument(
|
|
443
|
+
"--no-log", action="store_true",
|
|
444
|
+
help="Do not correlate openclaw application logs",
|
|
445
|
+
)
|
|
446
|
+
g.add_argument(
|
|
447
|
+
"--show-tool-metas", action="store_true",
|
|
448
|
+
help="Show full meta for each toolCall",
|
|
449
|
+
)
|
|
450
|
+
g.add_argument(
|
|
451
|
+
"--show-plugin-snapshot", action="store_true",
|
|
452
|
+
help="Show plugin snapshot (hooks/status)",
|
|
453
|
+
)
|
|
454
|
+
g.add_argument(
|
|
455
|
+
"--mask", action="store_true",
|
|
456
|
+
help="Force masking (trace does not mask by default)",
|
|
457
|
+
)
|
|
458
|
+
g.add_argument(
|
|
459
|
+
"--agent", default=None,
|
|
460
|
+
help="Limit to a specific agent",
|
|
461
|
+
)
|
|
364
462
|
_common_arguments(p)
|
|
365
463
|
return p
|
|
366
464
|
|
|
367
465
|
|
|
368
466
|
def _build_extract_parser() -> argparse.ArgumentParser:
|
|
467
|
+
desc_text = _desc("extract")
|
|
468
|
+
description = f"{desc_text} (extract)" if desc_text else "(extract)"
|
|
369
469
|
p = argparse.ArgumentParser(
|
|
370
470
|
prog="openclaw-diag extract",
|
|
471
|
+
description=description,
|
|
371
472
|
add_help=True,
|
|
372
473
|
epilog=_EXTRACT_EPILOG,
|
|
373
474
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
374
475
|
)
|
|
375
|
-
p.
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
476
|
+
g = p.add_argument_group("extract options")
|
|
477
|
+
g.add_argument(
|
|
478
|
+
"session_id",
|
|
479
|
+
help="Target session UUID (full or 8+ char prefix)",
|
|
480
|
+
)
|
|
481
|
+
g.add_argument(
|
|
482
|
+
"--summary", action="store_true",
|
|
483
|
+
help="Per-file record-count summary; do not dump record bodies",
|
|
484
|
+
)
|
|
485
|
+
g.add_argument(
|
|
486
|
+
"-a", "--all", action="store_true", dest="all_versions",
|
|
487
|
+
help="Export all versions (active + reset + deleted + backup)",
|
|
488
|
+
)
|
|
489
|
+
g.add_argument(
|
|
490
|
+
"--list", action="store_true", dest="list_only",
|
|
491
|
+
help="List matching files only; do not extract content",
|
|
492
|
+
)
|
|
493
|
+
g.add_argument(
|
|
494
|
+
"--types", default=None,
|
|
495
|
+
help="Filter by record type (comma-separated, e.g. user,assistant,toolCall)",
|
|
496
|
+
)
|
|
497
|
+
g.add_argument(
|
|
498
|
+
"--agent", default=None,
|
|
499
|
+
help="Limit to a specific agent",
|
|
500
|
+
)
|
|
385
501
|
_common_arguments(p)
|
|
386
502
|
return p
|
|
387
503
|
|
|
388
504
|
|
|
389
505
|
def _build_panorama_parser() -> argparse.ArgumentParser:
|
|
506
|
+
desc_text = _desc("panorama")
|
|
507
|
+
description = f"{desc_text} (panorama)" if desc_text else "(panorama)"
|
|
390
508
|
p = argparse.ArgumentParser(
|
|
391
509
|
prog="openclaw-diag panorama",
|
|
510
|
+
description=description,
|
|
392
511
|
add_help=True,
|
|
393
512
|
epilog=_PANORAMA_EPILOG,
|
|
394
513
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
395
514
|
)
|
|
396
|
-
p.
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
515
|
+
g = p.add_argument_group("panorama options")
|
|
516
|
+
g.add_argument(
|
|
517
|
+
"session_id",
|
|
518
|
+
help="Target session UUID (full or 8+ char prefix)",
|
|
519
|
+
)
|
|
520
|
+
g.add_argument(
|
|
521
|
+
"--mask", action="store_true",
|
|
522
|
+
help="Sanitize tool args / message content / api keys",
|
|
523
|
+
)
|
|
524
|
+
g.add_argument(
|
|
525
|
+
"--run-index", type=int, default=None,
|
|
526
|
+
help="Pick the Nth run (default: -1 = latest)",
|
|
527
|
+
)
|
|
528
|
+
g.add_argument(
|
|
529
|
+
"--all-runs", action="store_true",
|
|
530
|
+
help="Include every run in the session",
|
|
531
|
+
)
|
|
532
|
+
g.add_argument(
|
|
533
|
+
"--strict-correlation", action="store_true",
|
|
534
|
+
help="Match only on sessionId / runIds (drop sessionKey and toolCallId hits)",
|
|
535
|
+
)
|
|
536
|
+
g.add_argument(
|
|
537
|
+
"--agent", default=None,
|
|
538
|
+
help="Limit to a specific agent",
|
|
539
|
+
)
|
|
407
540
|
_common_arguments(p)
|
|
408
541
|
return p
|
|
409
542
|
|
|
@@ -411,16 +544,29 @@ def _build_panorama_parser() -> argparse.ArgumentParser:
|
|
|
411
544
|
def cmd_inspector(head: str, rest: List[str]) -> int:
|
|
412
545
|
inspector = registry.get(head)
|
|
413
546
|
if inspector is None or inspector.kind != "inspector":
|
|
414
|
-
print(f"Error:
|
|
547
|
+
print(f"Error: unknown inspector '{head}'", file=sys.stderr)
|
|
415
548
|
return EXIT_INPUT_ERROR
|
|
416
549
|
if head == "trace":
|
|
417
550
|
parser = _build_trace_parser()
|
|
418
551
|
ns = parser.parse_args(rest)
|
|
552
|
+
# --all-messages traces every user turn; it is mutually exclusive with
|
|
553
|
+
# the single-turn selectors. Reject the combination at parse time so
|
|
554
|
+
# the inspector never sees an ambiguous request.
|
|
555
|
+
if ns.all_messages and (
|
|
556
|
+
ns.msg_index is not None
|
|
557
|
+
or ns.msg_id is not None
|
|
558
|
+
or ns.msg_match is not None
|
|
559
|
+
):
|
|
560
|
+
parser.error(
|
|
561
|
+
"--all-messages/-A cannot be combined with "
|
|
562
|
+
"--msg-index/--msg-id/--msg-match",
|
|
563
|
+
)
|
|
419
564
|
kwargs = {
|
|
420
565
|
"session_id": ns.session_id,
|
|
421
566
|
"msg_index": ns.msg_index,
|
|
422
567
|
"msg_id": ns.msg_id,
|
|
423
568
|
"msg_match": ns.msg_match,
|
|
569
|
+
"all_messages": ns.all_messages,
|
|
424
570
|
"no_trajectory": ns.no_trajectory,
|
|
425
571
|
"no_log": ns.no_log,
|
|
426
572
|
"show_tool_metas": ns.show_tool_metas,
|
|
@@ -527,19 +673,61 @@ def _split_skip(rest: List[str]):
|
|
|
527
673
|
|
|
528
674
|
|
|
529
675
|
def _print_help() -> None:
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
676
|
+
"""Render the top-level --help.
|
|
677
|
+
|
|
678
|
+
Structure: intro -> usage -> health checks -> scan diagnostics (rendered
|
|
679
|
+
dynamically from the registry) -> object diagnostics -> helper commands
|
|
680
|
+
-> global options -> exit codes -> more. Scan-list ids/descriptions are
|
|
681
|
+
pulled from registry + _COMMAND_DESC so newly registered collectors are
|
|
682
|
+
never missed.
|
|
683
|
+
"""
|
|
684
|
+
state_ids = [c.id for c in registry.all_state()]
|
|
685
|
+
width = 16
|
|
686
|
+
lines: List[str] = []
|
|
687
|
+
lines.append(f"openclaw-diag v{__version__} — OpenClaw operations diagnostics CLI")
|
|
688
|
+
lines.append("")
|
|
689
|
+
lines.append("Scans config / logs / sessions to pinpoint connection, performance, cron,")
|
|
690
|
+
lines.append("plugin, and run issues in an OpenClaw deployment.")
|
|
691
|
+
lines.append("")
|
|
692
|
+
lines.append("Usage:")
|
|
693
|
+
lines.append(" openclaw-diag <command> [args...]")
|
|
694
|
+
lines.append(" openclaw-diag <command> --help Show detailed args for a command")
|
|
695
|
+
lines.append("")
|
|
696
|
+
lines.append("Health checks:")
|
|
697
|
+
lines.append(f" {'all':<{width}s}Run every scan-type diagnostic at once (recommended first step)")
|
|
698
|
+
lines.append(f" {'doctor':<{width}s}Check the runtime (Node / Python / OpenClaw readiness)")
|
|
699
|
+
lines.append("")
|
|
700
|
+
lines.append("Scan diagnostics (no args required):")
|
|
701
|
+
for sid in state_ids:
|
|
702
|
+
if sid == "doctor":
|
|
703
|
+
# doctor is already listed under Health checks; skip the duplicate.
|
|
704
|
+
continue
|
|
705
|
+
lines.append(f" {sid:<{width}s}{_desc(sid)}")
|
|
706
|
+
lines.append("")
|
|
707
|
+
lines.append("Object diagnostics (require a session uuid):")
|
|
708
|
+
lines.append(f" {'trace <uuid>':<{width}s}{_desc('trace')}")
|
|
709
|
+
lines.append(f" {'extract <uuid>':<{width}s}{_desc('extract')}")
|
|
710
|
+
lines.append(f" {'panorama <uuid>':<{width}s}{_desc('panorama')}")
|
|
711
|
+
lines.append("")
|
|
712
|
+
lines.append("Helper commands:")
|
|
713
|
+
lines.append(f" {'list':<{width}s}List all available diagnostics (supports --format json)")
|
|
714
|
+
lines.append(f" {'examples':<{width}s}Print common usage examples")
|
|
715
|
+
lines.append("")
|
|
716
|
+
lines.append("Global options (all commands):")
|
|
717
|
+
lines.append(" --format pretty|json|ndjson Output format (default: pretty)")
|
|
718
|
+
lines.append(" --json Alias for --format json")
|
|
719
|
+
lines.append(" --no-color Disable colored output")
|
|
720
|
+
lines.append(" --unmask Show raw (unmasked) sensitive content")
|
|
721
|
+
lines.append(" --config / --log-dir / --sessions-base / --openclaw-home Override default paths")
|
|
722
|
+
lines.append("")
|
|
723
|
+
lines.append("Exit codes:")
|
|
724
|
+
lines.append(" 0 OK (no warn/fail)")
|
|
725
|
+
lines.append(" 1 Warn or fail present")
|
|
726
|
+
lines.append(" 2 Input error (bad args/uuid)")
|
|
727
|
+
lines.append(" 3 Runtime error")
|
|
728
|
+
lines.append("")
|
|
729
|
+
lines.append("More: `openclaw-diag list` for all diagnostics, `openclaw-diag <command> --help` for details.")
|
|
730
|
+
print("\n".join(lines))
|
|
543
731
|
|
|
544
732
|
|
|
545
733
|
def main(argv: Optional[List[str]] = None) -> int:
|
|
@@ -600,9 +788,11 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
600
788
|
# parse_known_args (which would otherwise execute the diagnostic).
|
|
601
789
|
# parse_known_args is preserved to keep the existing lenient handling
|
|
602
790
|
# of unrecognized flags from external callers.
|
|
791
|
+
d = _desc(coll.id)
|
|
792
|
+
desc = f"{d} ({coll.id})" if d else f"({coll.id})"
|
|
603
793
|
cparser = argparse.ArgumentParser(
|
|
604
794
|
prog=f"openclaw-diag {head}",
|
|
605
|
-
description=
|
|
795
|
+
description=desc,
|
|
606
796
|
add_help=True,
|
|
607
797
|
)
|
|
608
798
|
_common_arguments(cparser)
|
|
@@ -611,8 +801,8 @@ def main(argv: Optional[List[str]] = None) -> int:
|
|
|
611
801
|
args, _ = cparser.parse_known_args(rest)
|
|
612
802
|
return cmd_run_collector(args, head)
|
|
613
803
|
|
|
614
|
-
print(f"Error:
|
|
615
|
-
print("
|
|
804
|
+
print(f"Error: unknown command '{head}'", file=sys.stderr)
|
|
805
|
+
print("Run `openclaw-diag list` to see all diagnostics.", file=sys.stderr)
|
|
616
806
|
return EXIT_INPUT_ERROR
|
|
617
807
|
|
|
618
808
|
|
package/package.json
CHANGED