openclaw-agent-dashboard 1.0.39 → 1.0.41

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.
Files changed (58) hide show
  1. package/dashboard/api/agent_config_api.py +28 -7
  2. package/dashboard/api/agents.py +48 -10
  3. package/dashboard/api/agents_config.py +5 -1
  4. package/dashboard/api/chains.py +25 -5
  5. package/dashboard/api/collaboration.py +10 -9
  6. package/dashboard/api/debug_paths.py +5 -1
  7. package/dashboard/api/error_analysis.py +29 -11
  8. package/dashboard/api/errors.py +37 -11
  9. package/dashboard/api/fortify_routes.py +108 -0
  10. package/dashboard/api/input_safety.py +60 -0
  11. package/dashboard/api/performance.py +73 -53
  12. package/dashboard/api/subagents.py +95 -99
  13. package/dashboard/api/timeline.py +24 -3
  14. package/dashboard/api/version.py +2 -0
  15. package/dashboard/api/websocket.py +9 -7
  16. package/dashboard/core/__init__.py +1 -0
  17. package/dashboard/core/config_fortify.py +125 -0
  18. package/dashboard/core/error_handler.py +488 -0
  19. package/dashboard/core/fallback_manager.py +81 -0
  20. package/dashboard/core/logging_config.py +217 -0
  21. package/dashboard/core/safe_api_error.py +76 -0
  22. package/dashboard/core/schemas/__init__.py +16 -0
  23. package/dashboard/core/schemas/base.py +43 -0
  24. package/dashboard/core/schemas/session_schema.py +40 -0
  25. package/dashboard/core/schemas/subagent_schema.py +23 -0
  26. package/dashboard/data/agent_config_manager.py +6 -4
  27. package/dashboard/data/chain_reader.py +16 -12
  28. package/dashboard/data/error_analyzer.py +15 -11
  29. package/dashboard/data/session_reader.py +268 -46
  30. package/dashboard/data/subagent_reader.py +74 -49
  31. package/dashboard/data/timeline_reader.py +35 -49
  32. package/dashboard/main.py +24 -2
  33. package/dashboard/mechanism_reader.py +4 -5
  34. package/dashboard/mechanisms.py +2 -2
  35. package/dashboard/pytest.ini +3 -0
  36. package/dashboard/requirements.txt +5 -0
  37. package/dashboard/status/cache_fp_probe.py +40 -0
  38. package/dashboard/status/status_cache.py +199 -72
  39. package/dashboard/status/status_calculator.py +50 -30
  40. package/dashboard/tests/conftest.py +87 -0
  41. package/dashboard/tests/test_api_contracts.py +372 -0
  42. package/dashboard/tests/test_bench_fortify.py +176 -0
  43. package/dashboard/tests/test_fortify.py +952 -0
  44. package/dashboard/utils/__init__.py +1 -0
  45. package/dashboard/utils/data_repair.py +210 -0
  46. package/dashboard/watchers/file_watcher.py +380 -77
  47. package/frontend-dist/assets/{index-cYIOn3Wq.css → index-BIZ2xHfw.css} +1 -1
  48. package/frontend-dist/assets/{index-DyRXGevD.js → index-Cnr0b02R.js} +1 -1
  49. package/frontend-dist/index.html +2 -2
  50. package/openclaw.plugin.json +1 -1
  51. package/package.json +1 -1
  52. package/dashboard/agents.py +0 -74
  53. package/dashboard/collaboration.py +0 -407
  54. package/dashboard/errors.py +0 -63
  55. package/dashboard/performance.py +0 -474
  56. package/dashboard/session_reader.py +0 -240
  57. package/dashboard/status_calculator.py +0 -121
  58. package/dashboard/subagent_reader.py +0 -232
@@ -1,6 +1,7 @@
1
1
  """
2
2
  会话读取器 - 读取 sessions/*.jsonl 和 sessions.json
3
3
  """
4
+ import hashlib
4
5
  import json
5
6
  import os
6
7
  from pathlib import Path
@@ -8,9 +9,73 @@ from typing import List, Dict, Any, Optional
8
9
 
9
10
 
10
11
  from data.config_reader import get_openclaw_root, normalize_openclaw_agent_id
12
+ from utils.data_repair import parse_session_jsonl_line
11
13
 
12
14
  _META_SESSION_INDEX_KEYS = frozenset({"entries", "version", "schema"})
13
15
 
16
+ # 校验报告:大文件仅哈希尾部,与 tail 读取策略一致;小文件全量哈希
17
+ _MAX_FULL_HASH_BYTES = 4 * 1024 * 1024
18
+ _TAIL_HASH_BYTES = 512 * 1024
19
+
20
+
21
+ def compute_session_file_integrity(path: Path) -> Dict[str, Any]:
22
+ """文件级完整性元数据:size、mtime、sha256(全文件或尾部窗口)。"""
23
+ try:
24
+ st = path.stat()
25
+ except OSError as e:
26
+ return {"path": str(path), "error": f"stat_failed:{e}"}
27
+ size = int(st.st_size)
28
+ mtime_ns = int(getattr(st, "st_mtime_ns", int(st.st_mtime * 1e9)))
29
+ out: Dict[str, Any] = {
30
+ "path": str(path.resolve()),
31
+ "size_bytes": size,
32
+ "mtime_ns": mtime_ns,
33
+ }
34
+ try:
35
+ if size == 0:
36
+ out["sha256"] = hashlib.sha256(b"").hexdigest()
37
+ out["hash_scope"] = "full"
38
+ return out
39
+ if size <= _MAX_FULL_HASH_BYTES:
40
+ with open(path, "rb") as f:
41
+ out["sha256"] = hashlib.sha256(f.read()).hexdigest()
42
+ out["hash_scope"] = "full"
43
+ else:
44
+ with open(path, "rb") as f:
45
+ f.seek(max(0, size - _TAIL_HASH_BYTES))
46
+ tail = f.read()
47
+ out["sha256"] = hashlib.sha256(tail).hexdigest()
48
+ out["hash_scope"] = "tail_512kb"
49
+ out["tail_hashed_bytes"] = len(tail)
50
+ except OSError as e:
51
+ out["error"] = f"read_failed:{e}"
52
+ return out
53
+
54
+
55
+ def resolve_validated_session_jsonl(agent_id: str, relative: str) -> Optional[Path]:
56
+ """将相对路径解析为 agents/{id}/sessions 下的 .jsonl,禁止逃逸。"""
57
+ if not relative or not relative.strip():
58
+ return None
59
+ aid = normalize_openclaw_agent_id(agent_id)
60
+ base = get_openclaw_root() / "agents" / aid / "sessions"
61
+ if not base.is_dir():
62
+ return None
63
+ try:
64
+ base_r = base.resolve()
65
+ except OSError:
66
+ return None
67
+ rel = Path(relative.strip())
68
+ if rel.is_absolute() or ".." in rel.parts:
69
+ return None
70
+ try:
71
+ cand = (base_r / rel).resolve()
72
+ cand.relative_to(base_r)
73
+ except ValueError:
74
+ return None
75
+ if not cand.is_file() or cand.suffix.lower() != ".jsonl":
76
+ return None
77
+ return cand
78
+
14
79
 
15
80
  def normalize_sessions_index(raw: Any) -> Dict[str, Dict[str, Any]]:
16
81
  """
@@ -68,6 +133,38 @@ def resolve_session_jsonl_path(sessions_dir: Path, entry: Dict[str, Any]) -> Opt
68
133
  return None
69
134
 
70
135
 
136
+ def _load_sessions_index_file(sessions_index: Path) -> Optional[Dict[str, Any]]:
137
+ """读取 sessions.json;可选 JSON Schema 校验(OPENCLAW_JSON_STRICT)。失败返回 None 并记录错误。"""
138
+ if not sessions_index.exists():
139
+ return None
140
+ try:
141
+ with open(sessions_index, "r", encoding="utf-8") as f:
142
+ data = json.load(f)
143
+ except (json.JSONDecodeError, OSError) as e:
144
+ from core.error_handler import record_error
145
+
146
+ record_error("parsing-error", str(e), "sessions_index", exc=e)
147
+ return None
148
+ if not isinstance(data, dict):
149
+ from core.error_handler import record_error
150
+
151
+ record_error("validation-error", "sessions index root is not an object", "sessions_index")
152
+ return None
153
+ from core.config_fortify import get_fortify_config
154
+ from core.schemas.base import SchemaValidator
155
+ from core.schemas.session_schema import sessions_index_schema
156
+
157
+ cfg = get_fortify_config()
158
+ vr = SchemaValidator(sessions_index_schema, strict=cfg.json_strict).validate(data)
159
+ if not vr.is_valid:
160
+ from core.error_handler import record_error
161
+
162
+ record_error("validation-error", vr.error_message, "sessions_index")
163
+ if cfg.json_strict:
164
+ return None
165
+ return data
166
+
167
+
71
168
  def get_agent_sessions_path(agent_id: str) -> Optional[Path]:
72
169
  """获取 Agent 的 sessions 目录"""
73
170
  sessions_path = get_openclaw_root() / "agents" / normalize_openclaw_agent_id(agent_id) / "sessions"
@@ -125,12 +222,9 @@ def get_recent_messages(agent_id: str, limit: int = 10) -> List[Dict[str, Any]]:
125
222
  line = line.strip()
126
223
  if not line:
127
224
  continue
128
- try:
129
- data = json.loads(line)
130
- if data.get('type') == 'message':
131
- messages.append(data.get('message', {}))
132
- except json.JSONDecodeError:
133
- continue
225
+ _, msg = parse_session_jsonl_line(line)
226
+ if msg is not None:
227
+ messages.append(msg)
134
228
  # 必须取尾部:原先在扫描到 limit 条就 break,会拿到「窗口内较早」的消息而非最新,导致 tool/ thinking 误判
135
229
  return messages[-limit:] if len(messages) > limit else messages
136
230
 
@@ -214,21 +308,17 @@ def get_session_updated_at(agent_id: str) -> int:
214
308
  sessions_index = get_openclaw_root() / "agents" / aid / "sessions" / "sessions.json"
215
309
  if not sessions_index.exists():
216
310
  return 0
217
-
218
- try:
219
- with open(sessions_index, 'r', encoding='utf-8') as f:
220
- data = json.load(f)
221
- if not isinstance(data, dict):
222
- return 0
223
- index_map = normalize_sessions_index(data)
224
- max_ts = 0
225
- for entry in index_map.values():
226
- ts = entry.get('updatedAt') or entry.get('lastMessageAt') or 0
227
- if isinstance(ts, (int, float)) and ts > max_ts:
228
- max_ts = int(ts)
229
- return max_ts
230
- except (json.JSONDecodeError, IOError):
311
+
312
+ data = _load_sessions_index_file(sessions_index)
313
+ if not data:
231
314
  return 0
315
+ index_map = normalize_sessions_index(data)
316
+ max_ts = 0
317
+ for entry in index_map.values():
318
+ ts = entry.get("updatedAt") or entry.get("lastMessageAt") or 0
319
+ if isinstance(ts, (int, float)) and ts > max_ts:
320
+ max_ts = int(ts)
321
+ return max_ts
232
322
 
233
323
 
234
324
  def has_recent_session_activity(agent_id: str, minutes: int = 5) -> bool:
@@ -269,15 +359,12 @@ def get_session_turns(agent_id: str, session_key: Optional[str] = None, limit: i
269
359
  session_file: Optional[Path] = None
270
360
  sessions_path = get_openclaw_root() / "agents" / aid / "sessions"
271
361
  if session_key:
272
- try:
273
- with open(sessions_index, 'r', encoding='utf-8') as f:
274
- index_data = json.load(f)
362
+ index_data = _load_sessions_index_file(sessions_index)
363
+ if index_data:
275
364
  index_map = normalize_sessions_index(index_data)
276
365
  entry = index_map.get(session_key)
277
366
  if entry:
278
367
  session_file = resolve_session_jsonl_path(sessions_path, entry)
279
- except (json.JSONDecodeError, IOError):
280
- pass
281
368
 
282
369
  if not session_file or not session_file.exists():
283
370
  session_file = get_latest_session_file(agent_id)
@@ -290,19 +377,18 @@ def get_session_turns(agent_id: str, session_key: Optional[str] = None, limit: i
290
377
 
291
378
  with open(session_file, 'r', encoding='utf-8') as f:
292
379
  for line in f:
380
+ envelope, msg = parse_session_jsonl_line(line.strip())
381
+ if not envelope or envelope.get('type') != 'message' or msg is None:
382
+ continue
383
+ role = msg.get('role')
384
+ if not role:
385
+ continue
386
+
293
387
  try:
294
- data = json.loads(line.strip())
295
- if data.get('type') != 'message':
296
- continue
297
- msg = data.get('message', {})
298
- role = msg.get('role')
299
- if not role:
300
- continue
301
-
302
388
  turn: Dict[str, Any] = {
303
389
  'turnIndex': turn_index,
304
390
  'role': role,
305
- 'timestamp': msg.get('timestamp') or data.get('timestamp'),
391
+ 'timestamp': msg.get('timestamp') or envelope.get('timestamp'),
306
392
  'content': [],
307
393
  'usage': msg.get('usage'),
308
394
  'stopReason': msg.get('stopReason'),
@@ -350,8 +436,8 @@ def get_session_turns(agent_id: str, session_key: Optional[str] = None, limit: i
350
436
 
351
437
  turns.append(turn)
352
438
  turn_index += 1
353
-
354
- except (json.JSONDecodeError, KeyError):
439
+
440
+ except (KeyError, TypeError):
355
441
  continue
356
442
 
357
443
  return turns[-limit:] if len(turns) > limit else turns
@@ -471,17 +557,14 @@ def get_recent_messages_with_timestamp(agent_id: str, limit: int = 10) -> List[D
471
557
  line = line.strip()
472
558
  if not line:
473
559
  continue
474
- try:
475
- data = json.loads(line)
476
- if data.get('type') == 'message':
477
- msg = data.get('message', {})
478
- messages.append({
479
- 'message': msg,
480
- 'timestamp': msg.get('timestamp', 0),
481
- 'data_timestamp': data.get('timestamp', ''),
482
- })
483
- except json.JSONDecodeError:
560
+ env, msg = parse_session_jsonl_line(line)
561
+ if msg is None:
484
562
  continue
563
+ messages.append({
564
+ 'message': msg,
565
+ 'timestamp': msg.get('timestamp', 0),
566
+ 'data_timestamp': (env or {}).get('timestamp', ''),
567
+ })
485
568
 
486
569
  return messages[-limit:] if len(messages) > limit else messages
487
570
 
@@ -545,3 +628,142 @@ def get_pending_tool_call_with_timestamp(agent_id: str) -> Optional[Dict[str, An
545
628
  return tool_calls[last_id]
546
629
 
547
630
  return None
631
+
632
+
633
+ def get_session_validation_report(
634
+ agent_id: str,
635
+ *,
636
+ relative_session_file: Optional[str] = None,
637
+ auto_repair: Optional[bool] = None,
638
+ include_details: bool = False,
639
+ max_lines: int = 1000,
640
+ ) -> Dict[str, Any]:
641
+ """Validate session JSONL for an agent; used by GET /api/data/validate."""
642
+ from core.config_fortify import get_fortify_config
643
+
644
+ aid = normalize_openclaw_agent_id(agent_id)
645
+ sessions_dir = get_openclaw_root() / "agents" / aid / "sessions"
646
+ sessions_index_path = sessions_dir / "sessions.json"
647
+ cfg = get_fortify_config()
648
+ read_path_policy = {
649
+ "memory_auto_repair_default": cfg.auto_repair_json,
650
+ "disk_write_back_enabled": cfg.auto_repair_write_back,
651
+ "note": "本报告仅校验与统计;读路径不自动写盘,写回仅在显式修复工具中且受 OPENCLAW_AUTO_REPAIR_WB 控制。",
652
+ }
653
+
654
+ session_file: Optional[Path] = None
655
+ if relative_session_file and relative_session_file.strip():
656
+ session_file = resolve_validated_session_jsonl(agent_id, relative_session_file)
657
+ if not session_file:
658
+ return {
659
+ "agent_id": agent_id,
660
+ "validation_passed": False,
661
+ "sessions_index_path": str(sessions_index_path)
662
+ if sessions_index_path.exists()
663
+ else None,
664
+ "session_file": None,
665
+ "session_file_query": relative_session_file.strip(),
666
+ "file_integrity": None,
667
+ "read_path_policy": read_path_policy,
668
+ "total_lines": 0,
669
+ "valid_messages": 0,
670
+ "repaired_messages": 0,
671
+ "errors": [
672
+ {
673
+ "type": "invalid_session_file",
674
+ "message": "path not found, not .jsonl, or escapes sessions dir",
675
+ }
676
+ ],
677
+ "repair_report": {
678
+ "repaired_count": 0,
679
+ "repair_success_rate": 1.0,
680
+ "failed_repairs": [],
681
+ },
682
+ }
683
+ else:
684
+ session_file = get_latest_session_file(agent_id)
685
+
686
+ if not session_file:
687
+ return {
688
+ "agent_id": agent_id,
689
+ "validation_passed": True,
690
+ "sessions_index_path": str(sessions_index_path)
691
+ if sessions_index_path.exists()
692
+ else None,
693
+ "session_file": None,
694
+ "session_file_query": None,
695
+ "file_integrity": None,
696
+ "read_path_policy": read_path_policy,
697
+ "total_lines": 0,
698
+ "valid_messages": 0,
699
+ "repaired_messages": 0,
700
+ "errors": [],
701
+ "repair_report": {"repaired_count": 0, "repair_success_rate": 1.0, "failed_repairs": []},
702
+ }
703
+
704
+ raw_lines = _read_tail_lines(session_file, max_lines)
705
+ errors: List[Dict[str, Any]] = []
706
+ valid_messages = 0
707
+ repaired_messages = 0
708
+ failed_repairs: List[Dict[str, Any]] = []
709
+
710
+ for i, line in enumerate(raw_lines):
711
+ stripped = line.strip()
712
+ if not stripped:
713
+ continue
714
+ env_probe, _ = parse_session_jsonl_line(
715
+ stripped, auto_repair=False, json_strict=False
716
+ )
717
+ if env_probe and env_probe.get("type") != "message":
718
+ continue
719
+ _env_strict, msg_strict = parse_session_jsonl_line(
720
+ stripped, auto_repair=False, json_strict=True
721
+ )
722
+ if msg_strict is not None:
723
+ valid_messages += 1
724
+ continue
725
+ _env_loose, msg_loose = parse_session_jsonl_line(
726
+ stripped,
727
+ auto_repair=auto_repair if auto_repair is not None else True,
728
+ json_strict=False,
729
+ )
730
+ if msg_loose is not None:
731
+ repaired_messages += 1
732
+ else:
733
+ failed_repairs.append({"line_index": i, "reason": "unparseable_or_invalid_schema"})
734
+ if include_details:
735
+ errors.append(
736
+ {
737
+ "type": "validation_failed",
738
+ "line_index_in_tail_window": i,
739
+ "reason": "unparseable_or_invalid_schema",
740
+ "sample": stripped[:200],
741
+ }
742
+ )
743
+
744
+ total_non_empty = sum(1 for ln in raw_lines if ln.strip())
745
+ validation_passed = len(failed_repairs) == 0
746
+ denom = repaired_messages + valid_messages
747
+ rate = 1.0 if denom == 0 else repaired_messages / max(denom, 1)
748
+
749
+ return {
750
+ "agent_id": agent_id,
751
+ "validation_passed": validation_passed,
752
+ "sessions_index_path": str(sessions_index_path)
753
+ if sessions_index_path.exists()
754
+ else None,
755
+ "session_file": str(session_file.resolve()),
756
+ "session_file_query": relative_session_file.strip() if relative_session_file else None,
757
+ "file_integrity": compute_session_file_integrity(session_file),
758
+ "read_path_policy": read_path_policy,
759
+ "tail_lines_scanned": max_lines,
760
+ "total_lines": total_non_empty,
761
+ "valid_messages": valid_messages,
762
+ "repaired_messages": repaired_messages,
763
+ "errors": errors,
764
+ "repair_report": {
765
+ "repaired_count": repaired_messages,
766
+ "repair_success_rate": rate,
767
+ "failed_repairs": failed_repairs,
768
+ },
769
+ }
@@ -6,7 +6,16 @@ from pathlib import Path
6
6
  from typing import List, Dict, Any, Optional
7
7
 
8
8
  from data.config_reader import get_openclaw_root, normalize_openclaw_agent_id
9
- from data.session_reader import normalize_sessions_index, resolve_session_jsonl_path
9
+ from data.session_reader import (
10
+ normalize_sessions_index,
11
+ resolve_session_jsonl_path,
12
+ _load_sessions_index_file,
13
+ )
14
+ from core.config_fortify import get_fortify_config
15
+ from core.error_handler import record_error
16
+ from core.schemas.base import SchemaValidator
17
+ from core.schemas.subagent_schema import subagent_runs_root_schema
18
+ from utils.data_repair import parse_session_jsonl_line
10
19
 
11
20
 
12
21
  def load_subagent_runs() -> List[Dict[str, Any]]:
@@ -17,13 +26,35 @@ def load_subagent_runs() -> List[Dict[str, Any]]:
17
26
  runs_path = get_openclaw_root() / "subagents" / "runs.json"
18
27
  if not runs_path.exists():
19
28
  return []
20
-
21
- with open(runs_path, 'r', encoding='utf-8') as f:
22
- data = json.load(f)
23
-
29
+
30
+ try:
31
+ with open(runs_path, 'r', encoding='utf-8') as f:
32
+ data = json.load(f)
33
+ except (json.JSONDecodeError, OSError) as e:
34
+ record_error("parsing-error", str(e), "subagent_runs")
35
+ return []
36
+
37
+ if not isinstance(data, dict):
38
+ return []
39
+
40
+ cfg = get_fortify_config()
41
+ vr = SchemaValidator(subagent_runs_root_schema, strict=cfg.json_strict).validate(data)
42
+ if not vr.is_valid:
43
+ record_error("validation-error", vr.error_message, "subagent_runs")
44
+ if cfg.json_strict:
45
+ return []
46
+
24
47
  runs = data.get('runs', {})
25
48
  if isinstance(runs, dict):
26
- return list(runs.values())
49
+ out: List[Dict[str, Any]] = []
50
+ for run_id, rec in runs.items():
51
+ if not isinstance(rec, dict):
52
+ continue
53
+ merged = dict(rec)
54
+ if not merged.get('runId'):
55
+ merged['runId'] = run_id
56
+ out.append(merged)
57
+ return out
27
58
  return runs if isinstance(runs, list) else []
28
59
 
29
60
 
@@ -118,8 +149,9 @@ def get_agent_output_for_run(child_session_key: str, max_chars: int = 10000) ->
118
149
  return None
119
150
 
120
151
  try:
121
- with open(sessions_index, 'r', encoding='utf-8') as f:
122
- index_data = json.load(f)
152
+ index_data = _load_sessions_index_file(sessions_index)
153
+ if not index_data:
154
+ return None
123
155
  index_map = normalize_sessions_index(index_data)
124
156
  entry = index_map.get(child_session_key)
125
157
  if not entry:
@@ -132,22 +164,18 @@ def get_agent_output_for_run(child_session_key: str, max_chars: int = 10000) ->
132
164
  last_text = None
133
165
  with open(session_path, 'r', encoding='utf-8') as f:
134
166
  for line in f:
135
- try:
136
- data = json.loads(line)
137
- if data.get('type') != 'message':
138
- continue
139
- msg = data.get('message', {})
140
- if msg.get('role') != 'assistant':
141
- continue
142
- content = msg.get('content', [])
143
- for c in content:
144
- if isinstance(c, dict) and c.get('type') == 'text':
145
- text = c.get('text', '')
146
- if text and text.strip():
147
- last_text = text
148
- break
149
- except (json.JSONDecodeError, KeyError):
167
+ envelope, msg = parse_session_jsonl_line(line)
168
+ if not envelope or envelope.get('type') != 'message' or msg is None:
150
169
  continue
170
+ if msg.get('role') != 'assistant':
171
+ continue
172
+ content = msg.get('content', [])
173
+ for c in content:
174
+ if isinstance(c, dict) and c.get('type') == 'text':
175
+ text = c.get('text', '')
176
+ if text and text.strip():
177
+ last_text = text
178
+ break
151
179
 
152
180
  if not last_text or not last_text.strip():
153
181
  return None
@@ -155,7 +183,7 @@ def get_agent_output_for_run(child_session_key: str, max_chars: int = 10000) ->
155
183
  return last_text[:max_chars] + '\n\n...(输出已截断)'
156
184
  return last_text
157
185
  except Exception as e:
158
- print(f"get_agent_output_for_run 失败: {e}")
186
+ record_error("io-error", str(e), "subagent_reader:get_agent_output_for_run", exc=e)
159
187
  return None
160
188
 
161
189
 
@@ -180,8 +208,9 @@ def get_agent_files_for_run(child_session_key: str) -> List[str]:
180
208
  return []
181
209
 
182
210
  try:
183
- with open(sessions_index, 'r', encoding='utf-8') as f:
184
- index_data = json.load(f)
211
+ index_data = _load_sessions_index_file(sessions_index)
212
+ if not index_data:
213
+ return []
185
214
  index_map = normalize_sessions_index(index_data)
186
215
  entry = index_map.get(child_session_key)
187
216
  if not entry:
@@ -196,31 +225,27 @@ def get_agent_files_for_run(child_session_key: str) -> List[str]:
196
225
 
197
226
  with open(session_path, 'r', encoding='utf-8') as f:
198
227
  for line in f:
199
- try:
200
- data = json.loads(line)
201
- if data.get('type') != 'message':
228
+ envelope, msg = parse_session_jsonl_line(line)
229
+ if not envelope or envelope.get('type') != 'message' or msg is None:
230
+ continue
231
+ if msg.get('role') != 'assistant':
232
+ continue
233
+ content = msg.get('content', [])
234
+ for c in content:
235
+ if not isinstance(c, dict) or c.get('type') != 'toolCall':
202
236
  continue
203
- msg = data.get('message', {})
204
- if msg.get('role') != 'assistant':
237
+ name = c.get('name', '')
238
+ if name not in file_tools:
205
239
  continue
206
- content = msg.get('content', [])
207
- for c in content:
208
- if not isinstance(c, dict) or c.get('type') != 'toolCall':
240
+ args = c.get('arguments', {})
241
+ if isinstance(args, str):
242
+ try:
243
+ args = json.loads(args)
244
+ except json.JSONDecodeError:
209
245
  continue
210
- name = c.get('name', '')
211
- if name not in file_tools:
212
- continue
213
- args = c.get('arguments', {})
214
- if isinstance(args, str):
215
- try:
216
- args = json.loads(args)
217
- except json.JSONDecodeError:
218
- continue
219
- path = args.get('path') or args.get('file_path')
220
- if path and isinstance(path, str) and path.strip():
221
- file_paths.append(path.strip())
222
- except (json.JSONDecodeError, KeyError):
223
- continue
246
+ path = args.get('path') or args.get('file_path')
247
+ if path and isinstance(path, str) and path.strip():
248
+ file_paths.append(path.strip())
224
249
 
225
250
  # 去重并保持顺序
226
251
  seen = set()
@@ -231,5 +256,5 @@ def get_agent_files_for_run(child_session_key: str) -> List[str]:
231
256
  result.append(p)
232
257
  return result
233
258
  except Exception as e:
234
- print(f"get_agent_files_for_run 失败: {e}")
259
+ record_error("io-error", str(e), "subagent_reader:get_agent_files_for_run", exc=e)
235
260
  return []