@yxzpro/agent-acta 2.14.4

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 (127) hide show
  1. package/LICENSE +21 -0
  2. package/LOGFORMATS.md +960 -0
  3. package/README.md +238 -0
  4. package/REFERENCE.md +1428 -0
  5. package/agent-acta-page.html +1998 -0
  6. package/agent-acta-server.mjs +1066 -0
  7. package/agent-acta-widget.html +724 -0
  8. package/archive.mjs +335 -0
  9. package/core/log.mjs +20 -0
  10. package/core/service.mjs +4285 -0
  11. package/hooks/cursor-usage-hook.mjs +89 -0
  12. package/mcp-server.mjs +372 -0
  13. package/package.json +68 -0
  14. package/page/dialog-add-agent.js +65 -0
  15. package/page/dialog-alert.js +159 -0
  16. package/page/dialog-archive.js +342 -0
  17. package/page/dialog-compare.js +238 -0
  18. package/page/dialog-ctxw.js +101 -0
  19. package/page/dialog-diag.js +72 -0
  20. package/page/dialog-heatmap.js +215 -0
  21. package/page/dialog-selftest.js +202 -0
  22. package/page/dialog-sess-timeline.js +108 -0
  23. package/page/dialog-stats.js +694 -0
  24. package/page/dialog-usage.js +158 -0
  25. package/page/log-card.js +277 -0
  26. package/page/log-view.js +273 -0
  27. package/page/sess-view.js +382 -0
  28. package/page/shared.js +408 -0
  29. package/page/sidebar.js +129 -0
  30. package/page/style.css +973 -0
  31. package/page/topbar.js +61 -0
  32. package/parsers/atomcode.mjs +321 -0
  33. package/parsers/buddy.mjs +272 -0
  34. package/parsers/buddyext.mjs +582 -0
  35. package/parsers/claude.mjs +496 -0
  36. package/parsers/cline.mjs +468 -0
  37. package/parsers/codearts.mjs +96 -0
  38. package/parsers/codex.mjs +188 -0
  39. package/parsers/comate.mjs +195 -0
  40. package/parsers/copilot.mjs +308 -0
  41. package/parsers/cursor.mjs +493 -0
  42. package/parsers/devindb.mjs +358 -0
  43. package/parsers/discovery.mjs +1230 -0
  44. package/parsers/doubao.mjs +406 -0
  45. package/parsers/dsh.mjs +377 -0
  46. package/parsers/gemini.mjs +267 -0
  47. package/parsers/generic.mjs +494 -0
  48. package/parsers/hermes.mjs +398 -0
  49. package/parsers/kilo.mjs +42 -0
  50. package/parsers/kimi.mjs +249 -0
  51. package/parsers/mimocode.mjs +531 -0
  52. package/parsers/minimax.mjs +503 -0
  53. package/parsers/openclaw.mjs +490 -0
  54. package/parsers/opencode.mjs +465 -0
  55. package/parsers/shared.mjs +418 -0
  56. package/parsers/trace.mjs +66 -0
  57. package/parsers/tracecode.mjs +348 -0
  58. package/parsers/traedb.mjs +528 -0
  59. package/parsers/zcode.mjs +356 -0
  60. package/plugin/client.js +51 -0
  61. package/plugin/cordis.patch.yml +5 -0
  62. package/plugin/plugin.mjs +220 -0
  63. package/search.mjs +554 -0
  64. package/tools/grab-trae-key.ps1 +265 -0
  65. package/vendor/element-plus-icons.js +3 -0
  66. package/vendor/element-plus.css +1 -0
  67. package/vendor/element-plus.dark.css +1 -0
  68. package/vendor/element-plus.full.min.js +65 -0
  69. package/vendor/element-plus.zh-cn.js +2 -0
  70. package/vendor/svg/antgroup-color.svg +1 -0
  71. package/vendor/svg/bytedance-color.svg +1 -0
  72. package/vendor/svg/chatglm-color.svg +1 -0
  73. package/vendor/svg/claude-color.svg +1 -0
  74. package/vendor/svg/claudecode-color.svg +1 -0
  75. package/vendor/svg/cline.svg +1 -0
  76. package/vendor/svg/codebuddy-color.svg +1 -0
  77. package/vendor/svg/codebuddy.svg +1 -0
  78. package/vendor/svg/codex-color.svg +1 -0
  79. package/vendor/svg/copilot.svg +1 -0
  80. package/vendor/svg/cursor.svg +1 -0
  81. package/vendor/svg/deepseek-color.svg +1 -0
  82. package/vendor/svg/deepseek-harness-whale.svg +8 -0
  83. package/vendor/svg/doubao-color.svg +1 -0
  84. package/vendor/svg/gemini-color.svg +1 -0
  85. package/vendor/svg/glmv-color.svg +1 -0
  86. package/vendor/svg/grok.svg +1 -0
  87. package/vendor/svg/hermesagent.svg +1 -0
  88. package/vendor/svg/huawei-color.svg +1 -0
  89. package/vendor/svg/hunyuan-color.svg +1 -0
  90. package/vendor/svg/kilocode-avatar.svg +1 -0
  91. package/vendor/svg/kilocode.svg +1 -0
  92. package/vendor/svg/kimi-color.svg +1 -0
  93. package/vendor/svg/kimi.svg +1 -0
  94. package/vendor/svg/logo-AtomGit-G-red.svg +3 -0
  95. package/vendor/svg/logo-AtomGit-dark.svg +4 -0
  96. package/vendor/svg/logo-AtomGit-light.svg +4 -0
  97. package/vendor/svg/meta-color.svg +1 -0
  98. package/vendor/svg/mimo.svg +66 -0
  99. package/vendor/svg/minimax-code-logo.svg +8 -0
  100. package/vendor/svg/minimax-color.svg +1 -0
  101. package/vendor/svg/moonshot.svg +1 -0
  102. package/vendor/svg/nex-color.svg +1 -0
  103. package/vendor/svg/nex.svg +1 -0
  104. package/vendor/svg/nvidia-color.svg +1 -0
  105. package/vendor/svg/openai.svg +1 -0
  106. package/vendor/svg/openclaw-color.svg +1 -0
  107. package/vendor/svg/openclaw.svg +1 -0
  108. package/vendor/svg/opencode.svg +1 -0
  109. package/vendor/svg/openrouter-color.svg +1 -0
  110. package/vendor/svg/qoder-color.svg +1 -0
  111. package/vendor/svg/qwen-color.svg +1 -0
  112. package/vendor/svg/sensenova-color.svg +1 -0
  113. package/vendor/svg/stepfun.svg +1 -0
  114. package/vendor/svg/trae-color.svg +1 -0
  115. package/vendor/svg/traework-v2.svg +1 -0
  116. package/vendor/svg/wenxin-color.svg +1 -0
  117. package/vendor/svg/windsurf.svg +1 -0
  118. package/vendor/svg/workbuddy-color.svg +29 -0
  119. package/vendor/svg/xiaomimimo.svg +1 -0
  120. package/vendor/svg/yuanbao-color.svg +1 -0
  121. package/vendor/svg/yuanbao.svg +1 -0
  122. package/vendor/svg/zai.svg +1 -0
  123. package/vendor/svg/zhipu-color.svg +1 -0
  124. package/vendor/vue.global.prod.js +9 -0
  125. package/widget/main.js +202 -0
  126. package/widget/package.json +7 -0
  127. package/widget/preload.js +11 -0
@@ -0,0 +1,188 @@
1
+ // ---------------- codex 解析器(~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl) ----------------
2
+ // 从 agent-acta-server.mjs 拆出(纯搬家,逻辑零改动)。导出 scanCodex / codexEntryContent
3
+ // + 口径函数(codexUserText / codexTokenUsage —— 详情与扫描共用同一套)。
4
+ //
5
+ // 事件流:session_meta(cwd) → event_msg/task_started(开轮, 带 model_context_window)
6
+ // → response_item/function_call|message(reasoning) → event_msg/token_count(逐次 LLM 用量)
7
+ // → event_msg/task_complete | turn_aborted(收轮)
8
+ // 用户真实输入**不能**从 response_item/message 取:那里面混着 <environment_context> /
9
+ // <permissions instructions> 等注入内容,且注入物同样是 role=user,字段上区分不出来。
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+ import { files, entries, addEntry, modelName, markKind, listDirCached, isDir, toText, trunc, readCompleteLines } from './shared.mjs';
13
+
14
+ export function codexIdBase(fp) { return 'x#' + fp.replace(/[\\/:]/g, '~') + '#'; }
15
+
16
+ // 用户输入的落点 codex 各版本不一致,已实测两种方言,都要认:
17
+ // 老:event_msg/user_message 的 message(本机 2026-09-11 那份 0.153.4 还是这种)
18
+ // 新:event_msg/item_completed 的 item(type=UserMessage)的 content
19
+ // —— Codex Desktop 的会话走这种,整份 rollout 里 user_message 一条都没有,
20
+ // 只认老的那支「用户输入」就恒为空(页面渲成「(空)」),而 token/耗时/AI 输出都在,
21
+ // 所以看起来像整轮坏了、其实只塌了这一格。
22
+ // 两种形态的正文都不必特判:toText 认内容块数组({content:[{type:'text',text}]})。
23
+ export function codexUserText(p) {
24
+ if (p.type === 'user_message') return toText(p.message);
25
+ if (p.type === 'item_completed' && p.item?.type === 'UserMessage') return toText(p.item);
26
+ return '';
27
+ }
28
+
29
+ export function codexTokenUsage(j) {
30
+ const u = j.payload?.info;
31
+ if (!u) return null;
32
+ const d = u.last_token_usage || u.total_token_usage; // last = 本次调用增量
33
+ if (!d) return null;
34
+ return { tin: d.input_tokens || 0, tout: (d.output_tokens || 0) + (d.reasoning_output_tokens || 0), tcache: (d.cached_input_tokens || 0) + (d.cache_write_input_tokens || 0) };
35
+ }
36
+
37
+ function emitCodexTurns(agent, fp, data, fromIdx) {
38
+ const idBase = codexIdBase(fp);
39
+ for (let i = fromIdx; i < data.turns.length; i++) {
40
+ const t = data.turns[i];
41
+ addEntry(idBase + t.idx, {
42
+ agent, project: data.cwd || '(未知项目)', session: data.sessionId || path.basename(fp).replace(/^rollout-|\.jsonl$/g, ''),
43
+ time: t.time, dur: Math.max(0, t.lastTs - t.time), status: t.err ? 'error' : 'ok',
44
+ tin: t.tin, tout: t.tout, tcache: t.tcache, total: t.tin + t.tout + t.tcache,
45
+ ctx: t.ctx || 0, rounds: t.calls, tools: t.tools, calls: t.calls,
46
+ models: t.models, preview: t.preview,
47
+ // R19:codex 的轮边界是显式的 —— task_complete 收轮(finished),turn_aborted 用户打断
48
+ //(aborted,不标红:主动打断 ≠ 失败)。
49
+ finished: !!t.finished, aborted: !!t.aborted, toolNames: t.toolNames || {},
50
+ }, { file: fp, turn: t.idx, kind: 'codex' });
51
+ }
52
+ }
53
+
54
+ export function scanCodexFile(agent, fp, names) {
55
+ let fst; try { fst = fs.statSync(fp); } catch { return false; }
56
+ const key = fp, m = fst.mtimeMs, s = fst.size;
57
+ const prev = files.get(key);
58
+ if (prev && prev.m === m && prev.s === s) {
59
+ if (prev.data?.turns?.length && !entries.has(codexIdBase(fp) + (prev.data.turns.length - 1))) emitCodexTurns(agent, fp, prev.data, 0);
60
+ return true;
61
+ }
62
+ const off = prev?.off || 0;
63
+ const data = prev?.data || { cwd: '', sessionId: '', turns: [], emitted: 0 };
64
+ if (s <= off) { files.set(key, { agent, kind: 'codex', m, s, off, data }); markKind('codex'); return true; }
65
+ try {
66
+ const rd = readCompleteLines(fp, off, s);
67
+ if (!rd) { files.set(key, { agent, kind: 'codex', m, s, off, data }); markKind('codex'); return true; }
68
+ const newOff = rd.newOff;
69
+ for (const line of rd.lines) {
70
+ if (!line.trim()) continue;
71
+ let j; try { j = JSON.parse(line); } catch { continue; }
72
+ const p = j.payload || {};
73
+ const ts = j.timestamp ? Date.parse(j.timestamp) : 0;
74
+ const cur = data.turns[data.turns.length - 1];
75
+ if (j.type === 'session_meta') {
76
+ if (p.cwd) data.cwd = p.cwd;
77
+ if (p.id) data.sessionId = p.id;
78
+ } else if (p.type === 'task_started') {
79
+ data.turns.push({
80
+ idx: data.turns.length, time: p.started_at ? p.started_at * 1000 : ts, lastTs: p.started_at ? p.started_at * 1000 : ts,
81
+ tin: 0, tout: 0, tcache: 0, tools: 0, calls: 0, ctx: p.model_context_window || 0,
82
+ models: [], mids: [], err: false, preview: '', user: '',
83
+ toolNames: {}, finished: false, aborted: false, // R19/R20
84
+ });
85
+ } else if (cur && (p.type === 'user_message' || p.type === 'item_completed')) {
86
+ // 两种方言都从 codexUserText 取;item_completed 里还夹着 AgentMessage,
87
+ // 它会返回空串,这里自然不落地(正文另有 response_item/task_complete 两支在管)
88
+ const text0 = codexUserText(p);
89
+ if (text0 && !cur.preview) { cur.preview = text0.replace(/\s+/g, ' ').slice(0, 300); cur.user = text0.slice(0, 3000); }
90
+ } else if (p.type === 'token_count' && cur) {
91
+ const u = codexTokenUsage(j);
92
+ if (u) { cur.tin += u.tin; cur.tout += u.tout; cur.tcache += u.tcache; cur.calls++; }
93
+ if (ts) cur.lastTs = ts;
94
+ } else if (j.type === 'turn_context' && cur) {
95
+ // 过 modelName 再进数组:codex 各版本对 model 的写法不一致(字符串 / {slug} / 带 $ref 的 schema),
96
+ // 非字符串直接 join 出去就是一串 [object Object](见 modelName 上方的说明)
97
+ const mdl = modelName(p.model || p.model_id);
98
+ if (mdl && !cur.models.includes(mdl)) cur.models.push(mdl);
99
+ } else if (j.type === 'response_item') {
100
+ if (!cur) continue;
101
+ if (p.type === 'function_call' || p.type === 'custom_tool_call') {
102
+ cur.tools++;
103
+ const nm = typeof p.name === 'string' && p.name ? p.name : '?';
104
+ cur.toolNames[nm] = (cur.toolNames[nm] || 0) + 1;
105
+ }
106
+ else if (p.type === 'message') {
107
+ const mdl = modelName(p.model);
108
+ if (mdl && !cur.models.includes(mdl)) cur.models.push(mdl);
109
+ }
110
+ } else if (p.type === 'turn_aborted' && cur) {
111
+ cur.aborted = true; // R19:用户主动打断,单独立标志,不混入 error 红
112
+ if (ts) cur.lastTs = ts;
113
+ } else if (p.type === 'task_complete' && cur) {
114
+ cur.finished = true;
115
+ if (ts) cur.lastTs = ts;
116
+ if (p.error) cur.err = true;
117
+ }
118
+ if (cur && ts && ts > cur.lastTs) cur.lastTs = ts;
119
+ }
120
+ files.set(key, { agent, kind: 'codex', m, s, off: newOff, data });
121
+ markKind('codex');
122
+ emitCodexTurns(agent, fp, data, Math.max(0, data.emitted - 1));
123
+ data.emitted = data.turns.length;
124
+ } catch {}
125
+ return true;
126
+ }
127
+
128
+ // 日期三层目录递归;用 listDirCached 做目录 mtime 短路
129
+ export function scanCodex(agent, root) {
130
+ const walk = (dir, depth) => {
131
+ const names = listDirCached(dir);
132
+ if (!names) return;
133
+ for (const n of names) {
134
+ const fp = path.join(dir, n);
135
+ if (n.endsWith('.jsonl')) { if (/^rollout-/.test(n)) scanCodexFile(agent, fp); continue; }
136
+ if (depth < 4 && isDir(fp)) walk(fp, depth + 1);
137
+ }
138
+ };
139
+ walk(root, 0);
140
+ }
141
+
142
+ // 详情:重读 rollout 文件按 src.turn 过滤(日志可能已被轮转/压缩,扫描缓存里没有正文)。
143
+ // 用户输入与 token 取数走扫描侧同一套函数(codexUserText / codexTokenUsage),
144
+ // 开轮也都按 task_started 计数 —— 两边口径对齐,详情不会跟卡片对不上账。
145
+ export function codexEntryContent(src, full) {
146
+ const fstate = files.get(src.file);
147
+ const version = fstate ? fstate.m + ':' + (fstate.off || 0) : null; // 与页面轮询比对的版本号
148
+ const lines = fs.readFileSync(src.file, 'utf8').split('\n');
149
+ let turnIdx = -1;
150
+ const out = { user: '', assistant: '', tools: [], calls: [], v: version };
151
+ const texts = [];
152
+ const byCall = new Map(); // call_id -> tool(精确配对 input/output)
153
+ for (const line of lines) {
154
+ if (!line.trim()) continue;
155
+ let j; try { j = JSON.parse(line); } catch { continue; }
156
+ const p = j.payload || {};
157
+ if (p.type === 'task_started') { turnIdx++; continue; }
158
+ if (turnIdx !== src.turn) { if (turnIdx > src.turn) break; continue; }
159
+ // 同扫描侧:认 user_message 与 item_completed 两种方言(codexUserText 里有两者的样本)
160
+ if (p.type === 'user_message' || p.type === 'item_completed') { if (!out.user) out.user = codexUserText(p); continue; }
161
+ if (p.type === 'token_count') {
162
+ const u = codexTokenUsage(j);
163
+ if (u) out.calls.push({ model: '', tin: u.tin, tout: u.tout, tcache: u.tcache, dur: 0 });
164
+ continue;
165
+ }
166
+ if (p.type === 'task_complete') { if (p.last_agent_message && !texts.length) texts.push(String(p.last_agent_message)); continue; }
167
+ if (j.type !== 'response_item') continue;
168
+ if (p.type === 'message' && p.role === 'assistant') {
169
+ for (const c of p.content || []) if (c && (c.text || c.type === 'output_text')) texts.push(String(c.text || ''));
170
+ } else if (p.type === 'function_call' || p.type === 'custom_tool_call') {
171
+ const fi = {};
172
+ const t = { name: p.name || p.tool_name || '?', tid: p.call_id || null, input: trunc(p.arguments ?? p.input, full, fi), inputTrunc: !!fi.t, output: '', error: null };
173
+ out.tools.push(t);
174
+ if (t.tid) byCall.set(t.tid, t);
175
+ } else if (p.type === 'function_call_output' || p.type === 'custom_tool_call_output') {
176
+ const t = (p.call_id && byCall.get(p.call_id)) || out.tools.find(x => !x.output);
177
+ if (t) {
178
+ const fo = {};
179
+ // 这里就是同事截图里那两行 `[object Object],[object Object]` 的入口:
180
+ // 较新版本的 codex 把 output 写成内容块数组,原来 trunc 里的 String() 直接把它串成了那样。
181
+ t.output = trunc(p.output ?? p.content, full, fo);
182
+ t.outputTrunc = !!fo.t;
183
+ }
184
+ }
185
+ }
186
+ out.assistant = texts.join('\n');
187
+ return out;
188
+ }
@@ -0,0 +1,195 @@
1
+ // ---------------- comate 解析器(百度 Comate / comate-kernel,.comate-engine/store 会话库) ----------------
2
+ // 首次接入(2026-09-24)。真源是 ~/.comate-engine/store/(本机 Windows 实测;IDE 侧
3
+ // %APPDATA%\Comate 是 Electron 壳,只有 state.vscdb 索引指针,不是会话正文):
4
+ // comate_chat_sessions.jsonl 会话索引(每次变更追加一行,同 uuid 多条、取最新)
5
+ // chat_session_<uuid> 会话正文,**整份 JSON 原地重写**(不是逐行追加!)
6
+ // blobs/chat_session_<uuid>.d/ 工具输出大对象的 blob(正文里以 {"$blob":"sha256:…"} 占位)
7
+ // <工作区名>-<hash>/ 按工作区隔离的小件(recentSkills 等,与轮次无关)
8
+ //
9
+ // 因此扫描走 cursor/gemini 同一路数:**整份重解析 + {mtime,size} 签名跳过没变的文件**,
10
+ // 不进 OFF_KINDS(没有字节偏移可续读 —— 文件被整个重写,off 语义不存在)。
11
+ //
12
+ // 会话文件结构(v2 实测):{ sessionUuid, title, ctime, utime, workspaceDirectory, messages[] }。
13
+ // 一轮 = 一条 user 消息(payload.query 是用户输入,requestedAt 是轮起点毫秒)+ 它后面那条
14
+ // assistant 消息(用 userMessageId 回指)。assistant 的 elements[] 是一棵两层的树:
15
+ // 顶层节点(无 type)= 一次「模型往返」,metrics.duration 是这段耗时;
16
+ // children = REASON(思考,不落详情)/ TEXT(正文片段)/ TOOL(工具调用)。
17
+ // tokenUsage 只有 contextUsed / contextLimit(上下文占用与容量),**没有**逐次 input/output ——
18
+ // 全 store 目录里 token 形状的键只有 tokenUsage 与 firstTokenLatency 两个,旁边 log/ 里搜到的
19
+ // 「token」全是 accessToken / semanticTokens 之类(凭证与 LSP 能力位),没有请求用量。
20
+ // 与 cursor 同样的处境:tin/tout 写 0、calls/rounds 写 null —— 页面那栏据此留空(`—`),不印假 0。
21
+ // 轮耗时取 assistant.completedAt(epoch 毫秒)减轮首;⚠️ elements 上的 lastModifiedTime/startTime
22
+ // 也是 epoch、metrics.duration 才是时长 —— 两个单位混过一次,卡片上就出现过 56 年的耗时(见 collectElements)。
23
+ // 源里另有两条本轮不用的:performanceMetrics.firstTokenLatency(TTFT,卡片没有渲染位、要它得先落 events[])、
24
+ // tokenUsage.needCompression(本机实测三条全 true,更像「此刻需要压缩」的状态位而不是「压缩发生过几次」,
25
+ // 拿去点 compacts 会造出「每轮都压缩」的假账)。
26
+ // 模型名在 user 消息 payload.model(displayName 可读名 + modelId),逐轮各异,取轮自己的。
27
+ import fs from 'node:fs';
28
+ import path from 'node:path';
29
+ import { files, srcs, entries, removedIds, sorted, addEntry, listDirCached, isDir, toText, trunc, toolNameCounts } from './shared.mjs';
30
+
31
+ const COMATE_NO_USAGE_NOTE = 'Comate 的会话文件只记上下文占用(contextUsed/contextLimit),没有逐次 input/output token,'
32
+ + '卡片那一栏留空(—)是「源里没有」,不是解析缺口;上下文占用取该轮结束时 assistant 消息上那份(会话级口径)。';
33
+
34
+ // 会话状态 → 轮状态。cancelled(用户打断)/ inProgress(进行中)/ success。
35
+ // failed / 其它未知值一律当 error,不做假设。
36
+ function comateTurnStatus(st) {
37
+ if (st === 'cancelled') return { aborted: true, finished: true, err: false };
38
+ if (st === 'inProgress') return { aborted: false, finished: false, err: false };
39
+ if (st === 'success') return { aborted: false, finished: true, err: false };
40
+ return { aborted: false, finished: true, err: true };
41
+ }
42
+
43
+ // 从 assistant 消息的 elements 树里收正文(TEXT 节点)与工具(TOOL 节点,含入参/输出/错误)。
44
+ // toolState:executed / executing / pending / failed —— 非 executed 的输出是空的,如实保留。
45
+ // ⚠️ 返回的是**两个不同单位**的量,别再混进一个变量(这里曾经错过一次):
46
+ // durSum —— 轮内各顶层节点 metrics.duration 之和,是**时长**(毫秒)
47
+ // endEpoch —— 元素上 lastModifiedTime / startTime 的最大值,是**时间戳**(epoch 毫秒)
48
+ // 轮耗时由调用方按「completedAt → endEpoch → time + durSum」的优先级算,见 comateSessionFile。
49
+ function collectElements(msg) {
50
+ const texts = [], tools = [];
51
+ let durSum = 0, endEpoch = 0;
52
+ for (const top of msg.elements || []) {
53
+ const dur = Number(top?.metrics?.duration) || 0;
54
+ if (dur > 0) durSum += dur; // 顶层节点顺序相接,耗时累加 ≈ 轮内时间推进
55
+ for (const el of (top.children || [top])) {
56
+ const type = el.type || '';
57
+ if (type === 'TEXT') {
58
+ const t = String(el.content || '').trim();
59
+ if (t) texts.push(t);
60
+ } else if (type === 'TOOL') {
61
+ const r = el.result || {};
62
+ tools.push({
63
+ name: el.toolName || '?', tid: el.id || null,
64
+ input: trunc(el.params ?? {}, true), inputTrunc: false,
65
+ output: trunc(r.output ?? '', true), outputTrunc: false,
66
+ error: el.toolState && el.toolState !== 'executed' ? (el.toolState) : null,
67
+ dur: Number(el?.metrics?.duration) || 0,
68
+ });
69
+ }
70
+ // REASON(思考过程)与其余未知类型不落详情,与其它 agent 口径一致
71
+ const st0 = Number(el.lastModifiedTime) || Number(el.startTime) || 0;
72
+ if (st0 > endEpoch) endEpoch = st0;
73
+ }
74
+ }
75
+ return { text: texts.join('\n\n---\n\n'), tools, durSum, endEpoch };
76
+ }
77
+
78
+ // 解析一份会话文件 → { sessionId, title, project, turns[] }。轮序 = user 消息序。
79
+ function comateSessionFile(fp) {
80
+ const j = JSON.parse(fs.readFileSync(fp, 'utf8'));
81
+ const sessionId = j.sessionUuid || path.basename(fp).replace(/^chat_session_/, '');
82
+ const turns = [];
83
+ // assistant 消息用 userMessageId 回指所属轮:先按 id 建索引,扫到 user 时直接取配对
84
+ const byUser = new Map();
85
+ for (const m of j.messages || []) if (m.role === 'assistant' && m.userMessageId) byUser.set(m.userMessageId, m);
86
+ for (const m of j.messages || []) {
87
+ if (m.role !== 'user') continue;
88
+ const query = String(m.payload?.query ?? m.content ?? '');
89
+ if (!query.trim()) continue; // 空注入不开轮
90
+ const a = byUser.get(m.id);
91
+ const time = Number(m.requestedAt) || Number(j.ctime) || 0;
92
+ const st = comateTurnStatus(a ? a.status : 'inProgress');
93
+ const col = a ? collectElements(a) : { text: '', tools: [], durSum: 0, endEpoch: 0 };
94
+ // 轮末时刻三档(都在**同一单位 epoch 毫秒**上比,谁都不许当时长用):
95
+ // ① assistant.completedAt —— 源里写的本轮收尾时刻,真值;
96
+ // ② 元素上的 lastModifiedTime/startTime 最大值 —— 只在它确实晚于轮首时采信;
97
+ // ③ 轮首 + Σ metrics.duration —— 前两档都没有时,用「各段时长之和」推一个末点。
98
+ // 三档全落空(进行中/被打断,压根没收尾)→ 末点就是轮首,dur = 0,页面显「—」,
99
+ // 而不是拿 epoch 当耗时倒出一个 56 年的数字(本机实测错值:dur=1790222269956ms)。
100
+ const completedAt = Number(a?.completedAt) || 0;
101
+ const end = completedAt > time ? completedAt
102
+ : (col.endEpoch > time ? col.endEpoch : (col.durSum > 0 ? time + col.durSum : time));
103
+ const mdl = m.payload?.model || {};
104
+ const model = mdl.displayName || mdl.modelId || '';
105
+ const usage = a?.tokenUsage || {};
106
+ turns.push({
107
+ session: sessionId, ord: turns.length,
108
+ user: query.slice(0, 3000), preview: query.replace(/\s+/g, ' ').slice(0, 300),
109
+ assistant: col.text, tools: col.tools,
110
+ time, lastTs: Math.max(end, time),
111
+ tin: 0, tout: 0, tcache: 0, nTools: col.tools.length, nCalls: 0,
112
+ models: model ? [model] : [],
113
+ ctx: Number(usage.contextLimit) || 0, ctxUsed: Number(usage.contextUsed) || 0,
114
+ ...st,
115
+ });
116
+ }
117
+ return { sessionId, title: String(j.title || ''), project: String(j.workspaceDirectory || ''), turns };
118
+ }
119
+
120
+ function emitComateTurns(agent, key, data, prevData) {
121
+ const idBase = 'C#' + key.replace(/[\\/:]/g, '~') + '#';
122
+ const ids = new Set();
123
+ for (let i = data.turns.length - 1; i >= 0; i--) {
124
+ const t = data.turns[i];
125
+ const id = idBase + t.session + '#' + t.ord;
126
+ ids.add(id);
127
+ entries.delete(id); srcs.delete(id); // 先删后插:重扫新增的轮不会漂到列表末尾(同 cursor)
128
+ addEntry(id, {
129
+ agent, project: data.project || '(未知项目)', session: t.session,
130
+ name: data.title,
131
+ time: t.time, dur: Math.max(0, t.lastTs - t.time),
132
+ status: t.err ? 'error' : 'ok',
133
+ tin: 0, tout: 0, tcache: 0, total: 0,
134
+ ctx: t.ctx || 0, ctxUsed: t.ctxUsed || null,
135
+ rounds: null, calls: null, // LLM 调用次数源里没有,不印假 0(同 cursor)
136
+ tools: t.nTools, finished: !!t.finished, aborted: !!t.aborted,
137
+ toolNames: toolNameCounts(t.tools), models: t.models, preview: t.preview,
138
+ }, { file: key, turn: i, kind: 'comate' });
139
+ }
140
+ for (const old of prevData?.ids || []) {
141
+ if (ids.has(old)) continue;
142
+ entries.delete(old); srcs.delete(old); removedIds.push(old);
143
+ }
144
+ data.ids = [...ids];
145
+ sorted.cache = null;
146
+ }
147
+
148
+ // root = ~/.comate-engine/store。会话正文是平铺的 chat_session_* 文件(无子目录)。
149
+ export function scanComate(agent, root) {
150
+ for (const n of listDirCached(root) || []) {
151
+ if (!n.startsWith('chat_session_')) continue; // 索引 jsonl / blobs / <工作区>-<hash>/ 一律不碰
152
+ const fp = path.join(root, n);
153
+ let st; try { st = fs.statSync(fp); } catch { continue; }
154
+ if (!st.isFile()) continue;
155
+ const key = 'comate:' + fp;
156
+ const sig = st.mtimeMs + '|' + st.size;
157
+ const prev = files.get(key);
158
+ if (prev && prev.data?.sig === sig) continue; // 整份重写式落盘:签名没变就一个字节都没变
159
+ try {
160
+ const r = comateSessionFile(fp);
161
+ const data = { sig, project: r.project, title: r.title, rev: (prev?.data?.rev || 0) + 1, turns: r.turns };
162
+ files.set(key, { agent, kind: 'comate', m: st.mtimeMs, s: st.size, off: 0, data });
163
+ emitComateTurns(agent, key, data, prev?.data);
164
+ } catch (e) { console.error('[comate]', n, e.message); }
165
+ }
166
+ }
167
+
168
+ // 详情:重读会话文件按 src.turn 切(与扫描同一份解析,口径天然一致)。
169
+ // 工具输出截断:full=0 时 800 字(与其它 agent 的详情口径一致),full=1 全量。
170
+ const COMATE_DETAIL_TRUNC = 800;
171
+ export function comateEntryContent(src, full) {
172
+ const fstate = files.get(src.file);
173
+ let r;
174
+ try { r = comateSessionFile(src.file.slice('comate:'.length)); } catch (e) {
175
+ return { user: '', assistant: '', tools: [], calls: [], v: null, note: '会话文件读取失败:' + e.message };
176
+ }
177
+ const t = r.turns[src.turn];
178
+ if (!t) return null;
179
+ const tools = (t.tools || []).map(x => {
180
+ const fi = {}, fo = {};
181
+ return {
182
+ name: x.name, tid: x.tid, error: x.error, dur: x.dur || 0,
183
+ input: trunc(x.input, full, fi), inputTrunc: !!fi.t,
184
+ output: full ? x.output : (typeof x.output === 'string' ? x.output.slice(0, COMATE_DETAIL_TRUNC) : x.output),
185
+ outputTrunc: !full && typeof x.output === 'string' && x.output.length > COMATE_DETAIL_TRUNC,
186
+ };
187
+ });
188
+ return {
189
+ user: t.user, assistant: t.assistant, tools, calls: [],
190
+ callsNote: COMATE_NO_USAGE_NOTE,
191
+ v: 'C' + (fstate?.data?.rev || 0),
192
+ };
193
+ }
194
+
195
+ export { toText };