@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,358 @@
1
+ // ---------------- devindb 型解析器(Devin CLI:%APPDATA%\Devin\cli\sessions.db / ~/.local/share/devin/cli/sessions.db) ----------------
2
+ // 数据源是**一个明文 SQLite 库**(WAL 模式,未加密),本机实测结构(backend_type='windsurf'):
3
+ // sessions(id, working_directory, backend_type, model, agent_mode, created_at, last_activity_at,
4
+ // title, main_chain_id, cogs_json, workspace_dirs, hidden, metadata)
5
+ // message_nodes(row_id PK, session_id, node_id, parent_node_id, chat_message(JSON), created_at, metadata)
6
+ // tool_call_state(session_id, tool_call_id, tool_call_json, tool_call_update_json)
7
+ // prompt_history / rendered_commits / subagent_heads 本机全空;app_state 只有 schema_compat_version;
8
+ // refinery_schema_history 是产品自己的迁移流水(17 条),都不进正文解析。
9
+ //
10
+ // **消息是一棵森林,不是一张表**:node_id/parent_node_id 连成树,sessions.main_chain_id 指向当前分支的
11
+ // 叶节点,沿 parent_node_id 往回走就是这一会话当前的主线。本机实测 261 个节点里只有 164 个在主线:
12
+ // 余下的全是「重新生成」留下的旁支(第一条 user 消息 #2/#9/#16 三份、#192 那份被顶掉的回复都在旁支里),
13
+ // 整表扫会把同一轮算三遍(旁支的 assistant/tool 节点与主线共享前缀)。所以**只走主线**。
14
+ //
15
+ // 轮口径:user 节点开轮 → 其后的 assistant 节点累加 → role='tool' 节点按 tool_call_id 回填上一条 call 的
16
+ // 出入参 → 下一条 user 收轮。中间穿插的 role='system' 节点(<additional_metadata> 之类)跳过;
17
+ // assistant 的 `thinking` 不是正文,进 others[]。
18
+ // 一次 assistant 节点 = **一次 LLM 调用**(与 hermes 的一行日志一次调用同构),它可能带 0..N 个
19
+ // tool_calls(实测有 2 个的),所以 calls 数 = assistant 节点数、tools 数 = tool_calls 总数。
20
+ // 收轮判据 = assistant 的 metadata.finish_reason === 'stop'(工具轮是 'tool_calls')。
21
+ //
22
+ // token 口径:**逐次精确值都在库里**,不需要辅助日志(assistant.metadata.metrics):
23
+ // input_tokens(**不含** cache)/ output_tokens / cache_read_tokens / cache_creation_tokens /
24
+ // ttft_ms / total_time_ms / tpot_ms。逐轮聚合 = 轮内各次相加;
25
+ // ctxUsed = 轮内最后一次调用的 input + cache_read + cache_creation。
26
+ // 上下文窗口容量源头没有 → ctx 恒 0(不编分母,与 hermes 同款纪律)。
27
+ // (根目录那批 py 探针断言「token 大多是 null、只在服务端」是错的:它们经 copy_db.bat 只拷了
28
+ // sessions.db、没拷 -wal,读到的是一份过期快照。)
29
+ //
30
+ // 时间:message_nodes.created_at(**列**)整库同一个值、不可用;真正的时间在 chat_message.metadata.created_at
31
+ // (ISO-8601 纳秒,UTC),user/assistant/tool 行同款,全站统一换成毫秒 epoch。
32
+ //
33
+ // 失败判据:工具失败在本轮工具行上(chisel/tool_result_meta.success === false,实测与 tool_call_state 的
34
+ // status='failed' 一一对应),只给那个工具打 ✗ —— 不把整轮染红(跑错一条命令在 devin 里太常见,
35
+ // 与 hermes「工具错即整轮 error」的取舍不同,这里源头有逐工具的明确标记,用不着连坐)。
36
+ // 轮级 error 只留给「这一轮一次都没结算」:后面还有新轮却没有任何一次带 metrics 的调用
37
+ // ⇒ 没拿到模型回复;被下一条 user 顶掉但调用是结算过的 ⇒ 用户打断(aborted,页面灰显,不算失败)。
38
+ //
39
+ // 只读纪律(与 hermes/opencode 同款):DatabaseSync {readOnly:true};签名 = sessions.db + -wal + -shm 的
40
+ // mtime/size,变了整库重读,没变跳过。内存态、不落 index,故不进 OFF_KINDS / PARSER_REV。
41
+ import fs from 'node:fs';
42
+ import path from 'node:path';
43
+ import {
44
+ entries, srcs, removedIds, sorted, addEntry, files,
45
+ toolNameCounts, modelName, toText, trunc, sqliteMod,
46
+ } from './shared.mjs';
47
+
48
+ export const devinDbErr = new Map();
49
+
50
+ function safeJson(raw) {
51
+ if (typeof raw !== 'string' || !raw) return null;
52
+ try { return JSON.parse(raw); } catch { return null; }
53
+ }
54
+ function num(v) {
55
+ const n = Number(v);
56
+ return Number.isFinite(n) ? n : 0;
57
+ }
58
+ // ISO-8601 → 毫秒 epoch。库里的纳秒精度("…T01:58:56.086899Z")**不能直接喂 Date.parse**:
59
+ // 规范只认到毫秒,多出来的小数位在部分运行时上直接 NaN;截到 3 位再解析,两边一致。
60
+ const ISO_RE = /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d+))?/;
61
+ function isoMs(s) {
62
+ const m = typeof s === 'string' ? ISO_RE.exec(s) : null;
63
+ if (!m) return 0;
64
+ const t = Date.parse(m[1] + '.' + (m[2] || '').padEnd(3, '0').slice(0, 3) + 'Z');
65
+ return Number.isFinite(t) ? t : 0;
66
+ }
67
+
68
+ function makeTurn(s, ts) {
69
+ return {
70
+ sid: s.id, time: ts || 0, lastTs: ts || 0,
71
+ model: s.model || '',
72
+ project: s.working_directory || '(未知项目)', session: s.id,
73
+ name: String(s.title || '').trim(),
74
+ user: '', texts: [], tools: [], byTool: new Map(),
75
+ callList: [], models: [], events: [], others: [],
76
+ tin: 0, tout: 0, tcache: 0, ctxUsed: 0,
77
+ err: false, errMsg: '', finished: false, aborted: false,
78
+ };
79
+ }
80
+
81
+ // 一个 assistant 节点 = 一次 LLM 调用;它的 tool_calls 全部登记成工具(可能有多个,并行调用)
82
+ function addCallFromNode(t, cm, ts) {
83
+ const md = cm.metadata || {};
84
+ const metrics = md.metrics || null;
85
+ const txt = toText(cm.content).trim();
86
+ const raw = Array.isArray(cm.tool_calls) ? cm.tool_calls : [];
87
+ const names = [];
88
+ for (const c of raw) {
89
+ const fn = c && typeof c === 'object' ? c : {};
90
+ const tool = {
91
+ name: String(fn.name || '?'),
92
+ tid: String(fn.id || ''),
93
+ ct: ts,
94
+ // arguments 在库里是**对象**(不是 JSON 串)——页面只认字符串,这里就地序列化
95
+ input: fn.arguments == null ? '' : (typeof fn.arguments === 'string' ? fn.arguments : JSON.stringify(fn.arguments, null, 2)),
96
+ output: '', error: null, dur: 0,
97
+ };
98
+ if (tool.tid) t.byTool.set(tool.tid, tool);
99
+ t.tools.push(tool);
100
+ names.push(tool.name);
101
+ }
102
+ // 真实模型名在每次调用的 generation_model 上(会话行的 model 是同一份预设);两者取到哪个用哪个
103
+ const mdl = modelName(md.generation_model) || modelName(t.model) || '';
104
+ const call = {
105
+ model: mdl || '(未知模型)',
106
+ provider: '',
107
+ tin: metrics ? num(metrics.input_tokens) : 0,
108
+ tout: metrics ? num(metrics.output_tokens) : 0,
109
+ tcache: metrics ? num(metrics.cache_read_tokens) : 0,
110
+ dur: metrics ? num(metrics.total_time_ms) : 0,
111
+ ttft: metrics ? num(metrics.ttft_ms) : 0,
112
+ finishReason: md.finish_reason && md.finish_reason !== 'stop' ? String(md.finish_reason) : undefined,
113
+ };
114
+ if (txt) call.text = txt;
115
+ if (names.length) call.tools = names;
116
+ t.callList.push(call);
117
+ if (mdl && !t.models.includes(mdl)) t.models.push(mdl);
118
+ if (metrics) {
119
+ t.tin += call.tin; t.tout += call.tout; t.tcache += call.tcache;
120
+ // 逐轮上下文 = 轮内最后一次**结算过**的调用的 prompt 全量(含缓存写入,口径同 claude 家族)
121
+ const ctx = call.tin + call.tcache + num(metrics.cache_creation_tokens);
122
+ if (ctx > 0) t.ctxUsed = ctx;
123
+ }
124
+ t.events.push({
125
+ k: 'llm', t: ts, ttft: call.ttft,
126
+ // 时间线把 llm 拆成「等待(ttft) + 解码(dur)」两段(见 sess-view):dur 给解码段,卡片上的 c.dur 才是整次
127
+ dur: metrics ? Math.max(0, call.dur - call.ttft) : 0,
128
+ model: call.model, tin: call.tin, tout: call.tout, tcache: call.tcache,
129
+ err: false, blocks: names.length ? ['tool_calls'] : (txt ? ['text'] : []),
130
+ finish: md.finish_reason || '',
131
+ });
132
+ return { call, settled: !!metrics };
133
+ }
134
+
135
+ function turnFromChain(s, chain) {
136
+ const turns = [];
137
+ let cur = null;
138
+ const pending = []; // [{ts, cm}]:assistant 节点,等 tool 行回填后再结算
139
+ for (const row of chain) {
140
+ const cm = safeJson(row.chat_message);
141
+ if (!cm) continue;
142
+ const md = cm.metadata || {};
143
+ const ts = isoMs(md.created_at);
144
+ if (cm.role === 'user') {
145
+ const text = toText(cm.content).trim();
146
+ if (!text) continue; // 空 user 不开轮(判据只写这一处,详情读同一份内存数据)
147
+ cur = makeTurn(s, ts);
148
+ cur.key = String(row.node_id); // 稳定键:轮里首条用户消息的节点号
149
+ cur.user = text.slice(0, 3000);
150
+ cur.events.push({ k: 'user', t: ts });
151
+ turns.push(cur);
152
+ continue;
153
+ }
154
+ if (!cur) continue; // 首条 user 之前的系统注入/规则载入不进轮
155
+ if (cm.role === 'system') continue; // 轮中间的系统注入(additional_metadata 之类)
156
+ if (cm.role === 'assistant') {
157
+ const thinking = cm.thinking && typeof cm.thinking.thinking === 'string' ? cm.thinking.thinking.trim() : '';
158
+ if (thinking) cur.others.push({ type: 'thinking', t: ts, json: thinking });
159
+ const txt = toText(cm.content).trim();
160
+ if (txt) cur.texts.push(txt);
161
+ const { settled } = addCallFromNode(cur, cm, ts);
162
+ cur.lastTs = Math.max(cur.lastTs, ts);
163
+ cur._settled = cur._settled || settled;
164
+ if (md.finish_reason === 'stop') cur._stop = true;
165
+ continue;
166
+ }
167
+ if (cm.role === 'tool') {
168
+ const tcid = String(cm.tool_call_id || '');
169
+ const tool = tcid ? cur.byTool.get(tcid) : null;
170
+ const out = toText(cm.content);
171
+ const ext = md.extensions || {};
172
+ const meta = ext['chisel/tool_result_meta'] || null;
173
+ const timing = ext['chisel/tool_call_timing'] || null;
174
+ const failed = !!(meta && meta.success === false);
175
+ if (tool) {
176
+ tool.output = out;
177
+ tool.dur = timing ? num(timing.duration_ms) : Math.max(0, ts - (tool.ct || ts));
178
+ if (failed) tool.error = 'error';
179
+ } else {
180
+ // 找不到对应的 call(该节点被裁掉等):不静默丢,单列一条只有返回的工具
181
+ const orphan = {
182
+ name: String(cm.tool_name || '?'), tid: tcid, ct: ts, input: '', output: out,
183
+ error: failed ? 'error' : null, dur: timing ? num(timing.duration_ms) : 0,
184
+ };
185
+ cur.tools.push(orphan);
186
+ if (tcid) cur.byTool.set(tcid, orphan);
187
+ }
188
+ cur.lastTs = Math.max(cur.lastTs, ts);
189
+ continue;
190
+ }
191
+ }
192
+ // 结算:收轮判据只看 finish_reason(与轮边界同一个来源),打断/失败在此定级
193
+ for (let i = 0; i < turns.length; i++) {
194
+ const t = turns[i];
195
+ const superseded = i + 1 < turns.length;
196
+ t.finished = !!t._stop || superseded;
197
+ if (!t._stop && t.finished) {
198
+ if (t._settled) t.aborted = true; // 结算过 ⇒ 用户打断(主动打断 ≠ 失败)
199
+ else { t.err = true; t.errMsg = '这一轮没有成功拿到模型回复(轮内没有任何一次调用结算)'; }
200
+ }
201
+ delete t._stop; delete t._settled;
202
+ t.endTs = t.lastTs;
203
+ t.events.push({ k: 'end', t: t.lastTs || t.time, reason: t.err ? 'error' : (t.aborted ? 'aborted' : 'completed') });
204
+ }
205
+ // call/result 事件(i 与 tools[] 同下标,页面直接复用 tools[i] 的出入参)
206
+ for (const t of turns) {
207
+ for (const [i, tool] of t.tools.entries()) {
208
+ t.events.push({ k: 'call', t: tool.ct || t.time, name: tool.name, tid: tool.tid || undefined, i });
209
+ t.events.push({ k: 'result', t: (tool.ct || t.time) + (tool.dur || 0), tid: tool.tid || undefined, i, dur: tool.dur || 0, error: !!tool.error });
210
+ }
211
+ t.events.sort((a, b) => (a.t || 0) - (b.t || 0));
212
+ t.preview = (t.user || t.texts.join('\n\n---\n\n') || t.errMsg || '').replace(/\s+/g, ' ').slice(0, 300);
213
+ }
214
+ return turns;
215
+ }
216
+
217
+ function dbSignature(dbPath) {
218
+ let dbSt;
219
+ try { dbSt = fs.statSync(dbPath); } catch { return null; }
220
+ let walSt = null, shmSt = null;
221
+ try { walSt = fs.statSync(dbPath + '-wal'); } catch {}
222
+ try { shmSt = fs.statSync(dbPath + '-shm'); } catch {}
223
+ return {
224
+ m: dbSt.mtimeMs, s: dbSt.size,
225
+ // 主库的 mtime/size 在 WAL 模式下可能长时间不动(新行只进 -wal),三个文件都要进签名
226
+ sig: [
227
+ dbSt.mtimeMs + '|' + dbSt.size,
228
+ walSt ? walSt.mtimeMs + '|' + walSt.size : '-',
229
+ shmSt ? shmSt.mtimeMs + '|' + shmSt.size : '-',
230
+ ].join('#'),
231
+ };
232
+ }
233
+
234
+ export function devinDbIdBase(dbPath) { return 'dv#' + dbPath.replace(/[\\/:]/g, '~') + '#'; }
235
+
236
+ function emit(agent, dbPath, data, prevData) {
237
+ const base = devinDbIdBase(dbPath);
238
+ const ids = new Set();
239
+ data.turns.forEach((t, i) => {
240
+ const id = base + t.sid.replace(/#/g, '~') + '#' + t.key;
241
+ ids.add(id);
242
+ addEntry(id, {
243
+ agent,
244
+ project: t.project,
245
+ session: t.session,
246
+ time: t.time,
247
+ dur: Math.max(0, (t.endTs || t.lastTs || t.time) - t.time),
248
+ status: t.err ? 'error' : 'ok',
249
+ tin: t.tin, tout: t.tout, tcache: t.tcache,
250
+ total: t.tin + t.tout + t.tcache,
251
+ ctx: 0, // 源头没有窗口容量 —— 不编分母
252
+ ctxUsed: t.ctxUsed || null,
253
+ rounds: t.callList.length,
254
+ calls: t.callList.length,
255
+ tools: t.tools.length,
256
+ models: t.models,
257
+ preview: t.preview,
258
+ name: t.name || null,
259
+ finished: !!t.finished,
260
+ aborted: !!t.aborted,
261
+ toolNames: toolNameCounts(t.tools),
262
+ }, { file: dbPath, turn: i, kind: 'devindb' });
263
+ });
264
+ for (const old of prevData?.ids || []) {
265
+ if (ids.has(old)) continue;
266
+ entries.delete(old); srcs.delete(old); removedIds.push(old);
267
+ }
268
+ data.ids = [...ids];
269
+ sorted.cache = null;
270
+ }
271
+
272
+ export function scanDevinDb(agent, dbPath) {
273
+ const stat = dbSignature(dbPath);
274
+ if (!stat) { devinDbErr.set(agent, '数据库不存在:' + dbPath); return; }
275
+ const prev = files.get(dbPath);
276
+ if (prev && prev.data?.sig === stat.sig) {
277
+ const last = prev.data.turns?.[prev.data.turns.length - 1];
278
+ if (last && !entries.has(devinDbIdBase(dbPath) + last.sid.replace(/#/g, '~') + '#' + last.key))
279
+ emit(agent, dbPath, prev.data, null);
280
+ return;
281
+ }
282
+ const mod = sqliteMod();
283
+ if (!mod) { devinDbErr.set(agent, '当前 Node 没有 node:sqlite(需要 22.5+)'); return; }
284
+ let db = null;
285
+ try {
286
+ db = new mod.DatabaseSync(dbPath, { readOnly: true });
287
+ // main_chain_id 为空的会话(还没落过轮)不查节点
288
+ const sessions = db.prepare(
289
+ 'SELECT id, working_directory, model, title, main_chain_id, hidden FROM sessions ORDER BY created_at, id'
290
+ ).all();
291
+ const nodes = db.prepare(
292
+ 'SELECT session_id, node_id, parent_node_id, chat_message FROM message_nodes ORDER BY session_id, node_id'
293
+ ).all();
294
+ const bySession = new Map();
295
+ for (const n of nodes) {
296
+ if (!bySession.has(n.session_id)) bySession.set(n.session_id, new Map());
297
+ bySession.get(n.session_id).set(n.node_id, n);
298
+ }
299
+ const turns = [];
300
+ for (const s of sessions) {
301
+ const map = bySession.get(s.id);
302
+ if (!map || s.main_chain_id == null) continue;
303
+ // 从叶往根回溯再翻正 —— 旁支因此天然被排除(它们不是 main_chain_id 的祖先)
304
+ const chain = [];
305
+ let cur = s.main_chain_id, guard = 0;
306
+ while (cur != null && map.has(cur) && guard++ < 100000) {
307
+ const n = map.get(cur);
308
+ chain.push(n);
309
+ cur = n.parent_node_id;
310
+ }
311
+ chain.reverse();
312
+ if (!chain.length) continue;
313
+ for (const t of turnFromChain(s, chain)) turns.push(t);
314
+ }
315
+ const data = { sig: stat.sig, turns, rev: (prev?.data?.rev || 0) + 1 };
316
+ files.set(dbPath, { agent, kind: 'devindb', m: stat.m, s: stat.s, off: 0, data });
317
+ devinDbErr.delete(agent);
318
+ emit(agent, dbPath, data, prev?.data);
319
+ } catch (e) {
320
+ console.error('[devindb]', e.message);
321
+ devinDbErr.set(agent, e.message);
322
+ } finally {
323
+ try { db?.close(); } catch {}
324
+ }
325
+ }
326
+
327
+ export function devinDbEntryContent(src, full) {
328
+ const st = files.get(src.file);
329
+ const t = st?.data?.turns?.[src.turn];
330
+ if (!t) return null;
331
+ const tools = t.tools.map(x => {
332
+ const fi = {}, fo = {};
333
+ return {
334
+ name: x.name, tid: x.tid, error: x.error, dur: x.dur || 0,
335
+ input: trunc(x.input, full, fi), inputTrunc: !!fi.t,
336
+ output: trunc(x.output, full, fo), outputTrunc: !!fo.t,
337
+ };
338
+ });
339
+ const calls = t.callList.map(c => {
340
+ if (!c.text) return { ...c };
341
+ const ft = {};
342
+ const text = trunc(c.text, full, ft);
343
+ return ft.t ? { ...c, text, textTrunc: true } : { ...c };
344
+ });
345
+ const out = {
346
+ user: t.user,
347
+ assistant: t.texts.join('\n\n---\n\n'),
348
+ tools, calls,
349
+ events: t.events || [],
350
+ others: (t.others || []).map(o => ({ type: o.type, t: o.t, json: full ? o.json : o.json.slice(0, 800) })),
351
+ v: 'dv' + (st.data.rev || 0),
352
+ };
353
+ if (t.err) out.callsNote = t.errMsg || '这一轮没有成功拿到模型回复';
354
+ return out;
355
+ }
356
+
357
+ // 判据在 discovery.mjs(devinDbVerify:sqlite 页头 + sessions/message_nodes/tool_call_state 三张独有表),
358
+ // 与 hermes/opencode 同一处摆放 —— 嗅探每 3s 走一轮,读盘量在那里控制。