openclaw-agent-dashboard 1.0.38 → 1.0.39
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.
|
@@ -151,6 +151,7 @@ _AGENT_ID_TO_LABEL = {
|
|
|
151
151
|
"analyst-agent": "分析师",
|
|
152
152
|
"architect-agent": "架构师",
|
|
153
153
|
"devops-agent": "运维",
|
|
154
|
+
"coder-agent": "开发",
|
|
154
155
|
"project-manager": "项目经理",
|
|
155
156
|
"test-agent": "测试",
|
|
156
157
|
"frontend-agent": "前端",
|
|
@@ -423,10 +424,15 @@ def _rebuild_subagent_timeline_payload(
|
|
|
423
424
|
result: Dict[str, Any],
|
|
424
425
|
limit: int,
|
|
425
426
|
round_mode: bool,
|
|
427
|
+
prefer_start: bool = False,
|
|
426
428
|
) -> None:
|
|
427
429
|
"""就地更新 result 的 steps / stats / rounds(步骤已为 dict 列表)。"""
|
|
428
430
|
if len(steps) > limit:
|
|
429
|
-
|
|
431
|
+
# 子任务需从「本次下发」起展示:保留 [:limit];主会话等仍用尾部 100
|
|
432
|
+
if prefer_start:
|
|
433
|
+
steps = steps[:limit]
|
|
434
|
+
else:
|
|
435
|
+
steps = steps[-limit:]
|
|
430
436
|
result['steps'] = _pair_tool_calls_and_results(steps)
|
|
431
437
|
total_duration = 0
|
|
432
438
|
total_input = 0
|
|
@@ -452,24 +458,60 @@ def _rebuild_subagent_timeline_payload(
|
|
|
452
458
|
result['roundMode'] = True
|
|
453
459
|
|
|
454
460
|
|
|
461
|
+
def _include_subagent_steps_before_run_anchor(
|
|
462
|
+
steps_in: List[Dict[str, Any]],
|
|
463
|
+
first_kept_index: int,
|
|
464
|
+
_anchor_ms: int,
|
|
465
|
+
cutoff: int,
|
|
466
|
+
) -> int:
|
|
467
|
+
"""
|
|
468
|
+
从 runs 取的 startedAt 可能略晚于「PM/主控下发 user」的落盘时间,且其之间常有 thinking/工具 步;
|
|
469
|
+
在第一个 ts>=cutoff 的步骤之前,把同一工作段内、早于 cutoff 的连续步骤并回(时间窗 20 分钟内)。
|
|
470
|
+
"""
|
|
471
|
+
if first_kept_index <= 0:
|
|
472
|
+
return 0
|
|
473
|
+
t_first = (steps_in[first_kept_index].get('timestamp') or 0) if first_kept_index < len(steps_in) else 0
|
|
474
|
+
if not t_first:
|
|
475
|
+
return first_kept_index
|
|
476
|
+
i0 = first_kept_index
|
|
477
|
+
# 在「首条已跨 cutoff」的步之前,并回同一 burst 中所有更早的步骤(含 PM 的 user 与紧挨的工具链)
|
|
478
|
+
while i0 > 0:
|
|
479
|
+
tlo = (steps_in[i0 - 1].get('timestamp') or 0)
|
|
480
|
+
if tlo >= cutoff:
|
|
481
|
+
i0 -= 1
|
|
482
|
+
continue
|
|
483
|
+
if (t_first - tlo) <= 20 * 60 * 1000:
|
|
484
|
+
i0 -= 1
|
|
485
|
+
else:
|
|
486
|
+
break
|
|
487
|
+
return i0
|
|
488
|
+
|
|
489
|
+
|
|
455
490
|
def _apply_subagent_run_anchor_to_result(
|
|
456
491
|
result: Dict[str, Any],
|
|
457
492
|
anchor_ms: int,
|
|
458
493
|
limit: int,
|
|
459
494
|
round_mode: bool,
|
|
460
495
|
) -> Dict[str, Any]:
|
|
461
|
-
"""去掉 anchor
|
|
496
|
+
"""去掉 anchor 之前无关步骤,并补回被锚点切掉的 PM/下发 user;若过滤后为空则保留原步骤。"""
|
|
462
497
|
steps_in = result.get('steps') or []
|
|
463
498
|
if not steps_in:
|
|
464
499
|
return result
|
|
465
500
|
cutoff = anchor_ms - _RUN_ANCHOR_SLACK_MS
|
|
466
|
-
|
|
501
|
+
first_i = next(
|
|
502
|
+
(i for i, s in enumerate(steps_in) if (s.get('timestamp') or 0) >= cutoff),
|
|
503
|
+
len(steps_in),
|
|
504
|
+
)
|
|
505
|
+
if first_i >= len(steps_in):
|
|
506
|
+
return result
|
|
507
|
+
i0 = _include_subagent_steps_before_run_anchor(steps_in, first_i, anchor_ms, cutoff)
|
|
508
|
+
filtered = steps_in[i0:]
|
|
467
509
|
if not filtered:
|
|
468
510
|
return result
|
|
469
511
|
result['runStartedAt'] = anchor_ms
|
|
470
512
|
if result.get('steps') and filtered[0].get('timestamp') is not None:
|
|
471
513
|
result['startedAt'] = filtered[0]['timestamp']
|
|
472
|
-
_rebuild_subagent_timeline_payload(filtered, result, limit, round_mode)
|
|
514
|
+
_rebuild_subagent_timeline_payload(filtered, result, limit, round_mode, prefer_start=True)
|
|
473
515
|
return result
|
|
474
516
|
|
|
475
517
|
|
|
@@ -605,7 +647,22 @@ def resolve_agent_session_jsonl(
|
|
|
605
647
|
sid = ent.get('sessionId') or preferred_key
|
|
606
648
|
return p, sid, preferred_key
|
|
607
649
|
|
|
608
|
-
# 2)
|
|
650
|
+
# 2) 按 sessions.json 的 updatedAt/lastMessageAt 选最近会话(在 glob mtime 之前)
|
|
651
|
+
# OpenClaw 在任务结束后可能从 runs.json 移除 run,此处仍可定位「最近活跃」子会话 jsonl。
|
|
652
|
+
# 多文件时比仅凭 *.jsonl 的 mtime 更稳,且与 4/24 当晚最晚更新 session 一致。
|
|
653
|
+
if agent_keys:
|
|
654
|
+
agent_keys.sort(
|
|
655
|
+
key=lambda k: (index_map[k].get('updatedAt') or index_map[k].get('lastMessageAt') or 0),
|
|
656
|
+
reverse=True,
|
|
657
|
+
)
|
|
658
|
+
for k in agent_keys:
|
|
659
|
+
ent = index_map[k]
|
|
660
|
+
p = resolve_session_jsonl_path(sessions_path, ent)
|
|
661
|
+
if p and p.is_file():
|
|
662
|
+
sid = ent.get('sessionId') or k
|
|
663
|
+
return p, sid, k
|
|
664
|
+
|
|
665
|
+
# 3) 无索引条目时:目录下最新的 *.jsonl(mtime)
|
|
609
666
|
jsonl_files = list(sessions_path.glob("*.jsonl"))
|
|
610
667
|
if jsonl_files:
|
|
611
668
|
jsonl_files.sort(key=lambda x: x.stat().st_mtime, reverse=True)
|
|
@@ -631,19 +688,6 @@ def resolve_agent_session_jsonl(
|
|
|
631
688
|
continue
|
|
632
689
|
return f, sid, resolved_key
|
|
633
690
|
|
|
634
|
-
# 3) 仅有 sessions.json 索引、尚无 glob 到的文件时:按 updatedAt 试解析路径
|
|
635
|
-
if agent_keys:
|
|
636
|
-
agent_keys.sort(
|
|
637
|
-
key=lambda k: (index_map[k].get('updatedAt') or index_map[k].get('lastMessageAt') or 0),
|
|
638
|
-
reverse=True,
|
|
639
|
-
)
|
|
640
|
-
for k in agent_keys:
|
|
641
|
-
ent = index_map[k]
|
|
642
|
-
p = resolve_session_jsonl_path(sessions_path, ent)
|
|
643
|
-
if p and p.is_file():
|
|
644
|
-
sid = ent.get('sessionId') or k
|
|
645
|
-
return p, sid, k
|
|
646
|
-
|
|
647
691
|
return None, None, None
|
|
648
692
|
|
|
649
693
|
|
|
@@ -686,12 +730,23 @@ def get_timeline_steps(
|
|
|
686
730
|
if session_file and session_file.exists():
|
|
687
731
|
req_key = session_key if session_key else resolved_key
|
|
688
732
|
requester_info = _get_requester_info_for_session(agent_id, req_key)
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
if
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
733
|
+
is_sub = not agent_ids_equal(agent_id, _get_main_agent_id())
|
|
734
|
+
subagent_anchor: Optional[int] = None
|
|
735
|
+
if is_sub:
|
|
736
|
+
subagent_anchor = _subagent_run_anchor_ms(agent_id, resolved_key)
|
|
737
|
+
result = _parse_session_file(
|
|
738
|
+
session_file,
|
|
739
|
+
agent_id,
|
|
740
|
+
session_id,
|
|
741
|
+
limit,
|
|
742
|
+
requester_info,
|
|
743
|
+
round_mode,
|
|
744
|
+
is_subagent=is_sub,
|
|
745
|
+
subagent_anchor_ms=subagent_anchor,
|
|
746
|
+
)
|
|
747
|
+
# 子 Agent:从 runs.json 本次 run 的 startedAt 起展示(含 PM/主控经链路下发到 coder 等子 agent)
|
|
748
|
+
if is_sub and subagent_anchor is not None:
|
|
749
|
+
result = _apply_subagent_run_anchor_to_result(result, subagent_anchor, limit, round_mode)
|
|
695
750
|
return result
|
|
696
751
|
if agent_ids_equal(agent_id, _get_main_agent_id()):
|
|
697
752
|
return _empty_main_agent_timeline(agent_id)
|
|
@@ -1224,33 +1279,147 @@ def _parse_session_lines(
|
|
|
1224
1279
|
return steps, started_at, session_status
|
|
1225
1280
|
|
|
1226
1281
|
|
|
1282
|
+
# 子 agent 会话 jsonl 全量读的安全上限(仅防极端大文件 OOM,主逻辑不据此判断「从哪开始」)
|
|
1283
|
+
_SUBAGENT_READ_SAFETY_BYTES = 32 * 1024 * 1024
|
|
1284
|
+
# 自「首条 user」起再读多少行入内存(有 runs 时仍从文件头解析以便锚点;无 runs 时从首条 user 行起)
|
|
1285
|
+
_MAX_LINES_AFTER_TASK_START = 20000
|
|
1286
|
+
|
|
1287
|
+
|
|
1288
|
+
def _line_index_of_first_user_message(path: Path) -> Optional[int]:
|
|
1289
|
+
"""
|
|
1290
|
+
扫描 jsonl,返回第一条 type=message 且 role=user 的所在行号(0-based)。
|
|
1291
|
+
用于「无 runs 锚点」时从 PM/主控下发(或 Subagent 首条 user)起构时序,而非按文件体积选 tail/head。
|
|
1292
|
+
"""
|
|
1293
|
+
try:
|
|
1294
|
+
with open(path, 'r', encoding='utf-8') as f:
|
|
1295
|
+
for i, line in enumerate(f):
|
|
1296
|
+
if '"role"' not in line or 'user' not in line:
|
|
1297
|
+
continue
|
|
1298
|
+
try:
|
|
1299
|
+
d = json.loads(line.strip())
|
|
1300
|
+
except json.JSONDecodeError:
|
|
1301
|
+
continue
|
|
1302
|
+
if d.get('type') != 'message':
|
|
1303
|
+
continue
|
|
1304
|
+
if (d.get('message') or {}).get('role') == 'user':
|
|
1305
|
+
return i
|
|
1306
|
+
except (OSError, IOError):
|
|
1307
|
+
pass
|
|
1308
|
+
return None
|
|
1309
|
+
|
|
1310
|
+
|
|
1311
|
+
def _read_text_line_window(path: Path, start_line: int, max_lines: int) -> List[str]:
|
|
1312
|
+
"""从第 start_line 行起,最多读 max_lines 行(供超大文件、无锚点时从首条 user 后解析)。"""
|
|
1313
|
+
out: List[str] = []
|
|
1314
|
+
try:
|
|
1315
|
+
with open(path, 'r', encoding='utf-8') as f:
|
|
1316
|
+
for i, line in enumerate(f):
|
|
1317
|
+
if i < start_line:
|
|
1318
|
+
continue
|
|
1319
|
+
out.append(line)
|
|
1320
|
+
if len(out) >= max_lines:
|
|
1321
|
+
break
|
|
1322
|
+
except (OSError, IOError):
|
|
1323
|
+
pass
|
|
1324
|
+
return out
|
|
1325
|
+
|
|
1326
|
+
|
|
1327
|
+
def _slice_subagent_steps_from_first_user(
|
|
1328
|
+
steps: List[TimelineStep],
|
|
1329
|
+
) -> List[TimelineStep]:
|
|
1330
|
+
"""从本会话中第一条 user(PM/主控下发到子 agent)起展示。"""
|
|
1331
|
+
for i, s in enumerate(steps):
|
|
1332
|
+
if s.type == StepType.USER.value:
|
|
1333
|
+
return steps[i:]
|
|
1334
|
+
return steps
|
|
1335
|
+
|
|
1336
|
+
|
|
1227
1337
|
def _parse_session_file(
|
|
1228
1338
|
session_file: Path,
|
|
1229
1339
|
agent_id: str,
|
|
1230
1340
|
session_id: Optional[str],
|
|
1231
1341
|
limit: int,
|
|
1232
1342
|
requester_info: Optional[Dict[str, str]] = None,
|
|
1233
|
-
round_mode: bool = True
|
|
1343
|
+
round_mode: bool = True,
|
|
1344
|
+
is_subagent: bool = False,
|
|
1345
|
+
subagent_anchor_ms: Optional[int] = None,
|
|
1234
1346
|
) -> Dict[str, Any]:
|
|
1235
|
-
"""
|
|
1347
|
+
"""
|
|
1348
|
+
解析 session jsonl;大文件对主 agent 仍可用尾部窗口。
|
|
1349
|
+
|
|
1350
|
+
子 agent:会话范围已由 resolve 指向「最近」jsonl。展示起点由语义决定——
|
|
1351
|
+
有 runs 锚点用 startedAt 对齐;无锚点时从首条 user(PM/主控下发)起。仅对超大文件用 _SUBAGENT_READ_SAFETY_BYTES
|
|
1352
|
+
与行数窗做**内存**保护,不作为「是否从 PM 起」的主判据。
|
|
1353
|
+
"""
|
|
1236
1354
|
path = session_file
|
|
1237
1355
|
header_ts = _read_session_header_timestamp(path)
|
|
1238
|
-
|
|
1239
|
-
if
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
)
|
|
1243
|
-
|
|
1356
|
+
step_budget = limit
|
|
1357
|
+
if is_subagent:
|
|
1358
|
+
step_budget = max(5000, min(limit * 50, 20000))
|
|
1359
|
+
try:
|
|
1360
|
+
file_size = path.stat().st_size
|
|
1361
|
+
except OSError:
|
|
1362
|
+
file_size = 0
|
|
1363
|
+
|
|
1364
|
+
steps: List[TimelineStep] = []
|
|
1365
|
+
started_at: Optional[int] = header_ts
|
|
1366
|
+
session_status = "completed"
|
|
1367
|
+
|
|
1368
|
+
if is_subagent:
|
|
1369
|
+
if file_size == 0:
|
|
1370
|
+
steps, started_at, session_status = _parse_session_lines(
|
|
1371
|
+
[], requester_info, started_at_hint=header_ts
|
|
1372
|
+
)
|
|
1373
|
+
elif file_size <= _SUBAGENT_READ_SAFETY_BYTES:
|
|
1244
1374
|
steps, started_at, session_status = _parse_session_lines(
|
|
1245
1375
|
_read_text_lines(path), requester_info, started_at_hint=header_ts
|
|
1246
1376
|
)
|
|
1377
|
+
if subagent_anchor_ms is None:
|
|
1378
|
+
steps = _slice_subagent_steps_from_first_user(steps)
|
|
1379
|
+
elif subagent_anchor_ms is not None:
|
|
1380
|
+
# 超大 + 有 run:以尾部为窗口(近期)再交给 get_timeline 的 _apply 锚定
|
|
1381
|
+
tail_lines = _read_jsonl_tail_line_slice(path)
|
|
1382
|
+
if tail_lines is not None:
|
|
1383
|
+
steps, started_at, session_status = _parse_session_lines(
|
|
1384
|
+
tail_lines, requester_info, started_at_hint=header_ts
|
|
1385
|
+
)
|
|
1386
|
+
else:
|
|
1387
|
+
steps, started_at, session_status = _parse_session_lines(
|
|
1388
|
+
_read_text_line_window(path, 0, _MAX_LINES_AFTER_TASK_START),
|
|
1389
|
+
requester_info, started_at_hint=header_ts
|
|
1390
|
+
)
|
|
1391
|
+
else:
|
|
1392
|
+
# 超大 + 无 run:先定位首条 user 行,自 PM/主控下发起读有限行
|
|
1393
|
+
uidx = _line_index_of_first_user_message(path)
|
|
1394
|
+
start = uidx if uidx is not None else 0
|
|
1395
|
+
part = _read_text_line_window(path, start, _MAX_LINES_AFTER_TASK_START)
|
|
1396
|
+
steps, started_at, session_status = _parse_session_lines(
|
|
1397
|
+
part, requester_info, started_at_hint=header_ts
|
|
1398
|
+
)
|
|
1399
|
+
steps = _slice_subagent_steps_from_first_user(steps)
|
|
1247
1400
|
else:
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1401
|
+
tail_lines = _read_jsonl_tail_line_slice(path)
|
|
1402
|
+
if tail_lines is not None:
|
|
1403
|
+
steps, started_at, session_status = _parse_session_lines(
|
|
1404
|
+
tail_lines, requester_info, started_at_hint=header_ts
|
|
1405
|
+
)
|
|
1406
|
+
if len(steps) < limit:
|
|
1407
|
+
steps, started_at, session_status = _parse_session_lines(
|
|
1408
|
+
_read_text_lines(path), requester_info, started_at_hint=header_ts
|
|
1409
|
+
)
|
|
1410
|
+
else:
|
|
1411
|
+
steps, started_at, session_status = _parse_session_lines(
|
|
1412
|
+
_read_text_lines(path), requester_info, started_at_hint=header_ts
|
|
1413
|
+
)
|
|
1251
1414
|
|
|
1252
|
-
if len(steps) >
|
|
1253
|
-
|
|
1415
|
+
if len(steps) > step_budget:
|
|
1416
|
+
if is_subagent:
|
|
1417
|
+
steps = steps[:step_budget]
|
|
1418
|
+
else:
|
|
1419
|
+
steps = steps[-limit:]
|
|
1420
|
+
|
|
1421
|
+
if is_subagent and subagent_anchor_ms is None and len(steps) > limit:
|
|
1422
|
+
steps = steps[:limit]
|
|
1254
1423
|
|
|
1255
1424
|
total_duration = 0
|
|
1256
1425
|
total_input = 0
|