openclaw-agent-dashboard 1.0.37 → 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.
- package/dashboard/api/collaboration.py +42 -35
- package/dashboard/data/session_reader.py +21 -14
- package/dashboard/data/timeline_reader.py +206 -37
- package/frontend-dist/assets/{index-CVnP0JTr.js → index-DyRXGevD.js} +8 -8
- package/frontend-dist/assets/{index-DsWSoANz.css → index-cYIOn3Wq.css} +1 -1
- package/frontend-dist/index.html +2 -2
- package/package.json +1 -1
|
@@ -73,6 +73,23 @@ class ActiveTask(BaseModel):
|
|
|
73
73
|
featureId: Optional[str] = None # FEATURE_ID(如果有)
|
|
74
74
|
|
|
75
75
|
|
|
76
|
+
def _main_agent_status_for_collaboration(main_agent_id: str) -> str:
|
|
77
|
+
"""
|
|
78
|
+
协作流程里 PM 卡片状态:与 delegate 边是否激活同源——只认 subagents/runs.json 的活跃 run
|
|
79
|
+
(is_agent_working)+ 近期会话错误;不使用 calculate_agent_status 里的主会话 solo 启发式
|
|
80
|
+
(thinking / 未完成 tool / sessions 短窗),避免「连线已变实线仍显示工作中」。
|
|
81
|
+
全局 /api/agents 仍用 calculate_agent_status,可继续反映「主会话自己在跑」的语义。
|
|
82
|
+
"""
|
|
83
|
+
from data.session_reader import has_recent_errors
|
|
84
|
+
from data.subagent_reader import is_agent_working
|
|
85
|
+
|
|
86
|
+
if has_recent_errors(main_agent_id, minutes=5):
|
|
87
|
+
return 'error'
|
|
88
|
+
if is_agent_working(main_agent_id):
|
|
89
|
+
return 'working'
|
|
90
|
+
return 'idle'
|
|
91
|
+
|
|
92
|
+
|
|
76
93
|
def _extract_feature_id(task_name: str) -> Optional[str]:
|
|
77
94
|
"""从任务名称中提取 FEATURE_ID"""
|
|
78
95
|
if not task_name:
|
|
@@ -149,6 +166,14 @@ def _build_agent_active_tasks(
|
|
|
149
166
|
if requester_agent_id and not agent_ids_equal(requester_agent_id, child_agent_id):
|
|
150
167
|
task_item['childAgentId'] = child_agent_id
|
|
151
168
|
|
|
169
|
+
# 自派:派发者与执行者为同一 Agent(如 PM 派给自己)时,同一 run 只记一条,否则会显示「2 个并行任务」
|
|
170
|
+
if requester_agent_id and agent_ids_equal(requester_agent_id, child_agent_id):
|
|
171
|
+
if child_agent_id not in agent_active_tasks:
|
|
172
|
+
agent_active_tasks[child_agent_id] = []
|
|
173
|
+
self_task = {k: v for k, v in task_item.items() if k != 'childAgentId'}
|
|
174
|
+
agent_active_tasks[child_agent_id].append(self_task)
|
|
175
|
+
continue
|
|
176
|
+
|
|
152
177
|
# 1. 添加到派发者(如果派发者是某个已知 agent)
|
|
153
178
|
if requester_agent_id:
|
|
154
179
|
if requester_agent_id not in agent_active_tasks:
|
|
@@ -197,8 +222,6 @@ class CollaborationFlow(BaseModel):
|
|
|
197
222
|
depths: Optional[Dict[str, int]] = None # agentId -> 层级深度 (0=主, 1=子, 2=孙...)
|
|
198
223
|
# 多任务并行展示:每个 Agent 的活跃任务列表
|
|
199
224
|
agentActiveTasks: Optional[Dict[str, List[ActiveTask]]] = None
|
|
200
|
-
# 多任务并行展示:每个 Agent 的活跃任务列表
|
|
201
|
-
agentActiveTasks: Optional[Dict[str, List[ActiveTask]]] = None
|
|
202
225
|
|
|
203
226
|
|
|
204
227
|
def _parse_agent_id(session_key: str) -> str:
|
|
@@ -340,30 +363,11 @@ def _enrich_main_agent_active_tasks_if_needed(
|
|
|
340
363
|
main_agent_id: str,
|
|
341
364
|
) -> Dict[str, List[Dict[str, Any]]]:
|
|
342
365
|
"""
|
|
343
|
-
|
|
344
|
-
|
|
366
|
+
PM 协作状态已与 runs 对齐;不再在「无 subagent run」时用 calculate_agent_status
|
|
367
|
+
伪造一条主会话任务,否则会出现徽章已空闲、卡片仍显示「当前任务」的矛盾。
|
|
368
|
+
主会话 solo 执行中的语义由 /api/agents 等入口展示。
|
|
345
369
|
"""
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
if calculate_agent_status(main_agent_id) != 'working':
|
|
349
|
-
return agent_active_tasks
|
|
350
|
-
if agent_active_tasks.get(main_agent_id):
|
|
351
|
-
return agent_active_tasks
|
|
352
|
-
hint = get_current_task(main_agent_id)
|
|
353
|
-
name = _clean_task_name(hint) if hint else ''
|
|
354
|
-
if not name:
|
|
355
|
-
return agent_active_tasks
|
|
356
|
-
merged = dict(agent_active_tasks)
|
|
357
|
-
merged[main_agent_id] = [
|
|
358
|
-
{
|
|
359
|
-
'id': 'task-main-session',
|
|
360
|
-
'name': name,
|
|
361
|
-
'status': 'working',
|
|
362
|
-
'timestamp': None,
|
|
363
|
-
'featureId': None,
|
|
364
|
-
}
|
|
365
|
-
]
|
|
366
|
-
return merged
|
|
370
|
+
return agent_active_tasks
|
|
367
371
|
|
|
368
372
|
|
|
369
373
|
def _get_agent_error_info(agent_id: str) -> Optional[Dict[str, Any]]:
|
|
@@ -574,14 +578,8 @@ async def get_collaboration():
|
|
|
574
578
|
recent_calls = _get_recent_model_calls(30)
|
|
575
579
|
|
|
576
580
|
main_display_name = (main_agent_config.get('name') if main_agent_config else None) or "主 Agent"
|
|
577
|
-
#
|
|
578
|
-
|
|
579
|
-
if main_raw == 'down':
|
|
580
|
-
main_status = 'error'
|
|
581
|
-
elif main_raw == 'working':
|
|
582
|
-
main_status = 'working'
|
|
583
|
-
else:
|
|
584
|
-
main_status = 'idle'
|
|
581
|
+
# PM:与连线 activePath 一致,仅用 runs 活跃 + 会话错误(见 _main_agent_status_for_collaboration)
|
|
582
|
+
main_status = _main_agent_status_for_collaboration(main_agent_id)
|
|
585
583
|
|
|
586
584
|
# 获取主 agent 的当前任务和错误信息
|
|
587
585
|
main_current_task = ''
|
|
@@ -909,8 +907,17 @@ async def get_collaboration_dynamic():
|
|
|
909
907
|
except Exception as e:
|
|
910
908
|
logger.warning(f"Failed to get display status for {aid}: {e}")
|
|
911
909
|
|
|
912
|
-
#
|
|
913
|
-
|
|
910
|
+
# PM:覆盖为与连线同源的状态;并修正 dynamic 文案,避免 calculate_agent_status 的 solo 与卡片矛盾
|
|
911
|
+
main_collab = _main_agent_status_for_collaboration(main_agent_id)
|
|
912
|
+
agent_statuses[main_agent_id] = main_collab
|
|
913
|
+
if main_collab == 'idle':
|
|
914
|
+
agent_dynamic_statuses[main_agent_id] = AgentDisplayStatus(
|
|
915
|
+
status='idle', display='空闲', duration=0, alert=False
|
|
916
|
+
)
|
|
917
|
+
elif main_collab == 'error':
|
|
918
|
+
agent_dynamic_statuses[main_agent_id] = AgentDisplayStatus(
|
|
919
|
+
status='idle', display='检测到错误', duration=0, alert=True
|
|
920
|
+
)
|
|
914
921
|
|
|
915
922
|
# 处理活跃任务(简化:不在流程图中创建任务节点,任务信息由 agentActiveTasks 提供)
|
|
916
923
|
# 任务详情在 Agent 卡片内显示,流程图只显示 Agent 之间的委托关系
|
|
@@ -129,10 +129,9 @@ def get_recent_messages(agent_id: str, limit: int = 10) -> List[Dict[str, Any]]:
|
|
|
129
129
|
data = json.loads(line)
|
|
130
130
|
if data.get('type') == 'message':
|
|
131
131
|
messages.append(data.get('message', {}))
|
|
132
|
-
if len(messages) >= limit:
|
|
133
|
-
break
|
|
134
132
|
except json.JSONDecodeError:
|
|
135
133
|
continue
|
|
134
|
+
# 必须取尾部:原先在扫描到 limit 条就 break,会拿到「窗口内较早」的消息而非最新,导致 tool/ thinking 误判
|
|
136
135
|
return messages[-limit:] if len(messages) > limit else messages
|
|
137
136
|
|
|
138
137
|
|
|
@@ -416,16 +415,26 @@ def get_latest_tool_call(agent_id: str) -> Optional[Dict[str, Any]]:
|
|
|
416
415
|
|
|
417
416
|
|
|
418
417
|
def has_thinking_block(agent_id: str) -> bool:
|
|
419
|
-
"""
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
418
|
+
"""
|
|
419
|
+
是否处于「当前回合的思考阶段」。
|
|
420
|
+
仅看会话中**最后一条**消息:已完成回合的 assistant 往往在 content 里仍保留 thinking 块,
|
|
421
|
+
若仍按「最近任意 assistant 含 thinking」会长期误判为工作中。
|
|
422
|
+
"""
|
|
423
|
+
messages = get_recent_messages(agent_id, limit=24)
|
|
424
|
+
if not messages:
|
|
425
|
+
return False
|
|
426
|
+
last = messages[-1]
|
|
427
|
+
if last.get('role') != 'assistant':
|
|
428
|
+
return False
|
|
429
|
+
# 已结束的一轮通常带 stopReason;此时 content 里的 thinking 只算历史,不算仍在思考
|
|
430
|
+
if last.get('stopReason'):
|
|
431
|
+
return False
|
|
432
|
+
content = last.get('content', [])
|
|
433
|
+
if isinstance(content, str):
|
|
434
|
+
return False
|
|
435
|
+
for c in content:
|
|
436
|
+
if isinstance(c, dict) and c.get('type') == 'thinking':
|
|
437
|
+
return True
|
|
429
438
|
return False
|
|
430
439
|
|
|
431
440
|
|
|
@@ -471,8 +480,6 @@ def get_recent_messages_with_timestamp(agent_id: str, limit: int = 10) -> List[D
|
|
|
471
480
|
'timestamp': msg.get('timestamp', 0),
|
|
472
481
|
'data_timestamp': data.get('timestamp', ''),
|
|
473
482
|
})
|
|
474
|
-
if len(messages) >= limit:
|
|
475
|
-
break
|
|
476
483
|
except json.JSONDecodeError:
|
|
477
484
|
continue
|
|
478
485
|
|
|
@@ -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
|