opencode-metrics-plugin 0.1.5 → 0.2.0
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/README.md +108 -390
- package/dist/index.d.ts +5 -51
- package/dist/index.js +5 -61
- package/dist/metrics/dirs.d.ts +23 -0
- package/dist/{dirs.js → metrics/dirs.js} +4 -10
- package/dist/metrics/engine/engine.d.ts +15 -0
- package/dist/{metrics-engine.js → metrics/engine/engine.js} +33 -146
- package/dist/{metrics-handlers.d.ts → metrics/engine/handlers.d.ts} +1 -1
- package/dist/{metrics-handlers.js → metrics/engine/handlers.js} +2 -2
- package/dist/metrics/engine/state.d.ts +79 -0
- package/dist/{metrics-types.js → metrics/engine/state.js} +1 -19
- package/dist/{event-logger.d.ts → metrics/eventlog/event-logger.d.ts} +0 -5
- package/dist/{event-logger.js → metrics/eventlog/event-logger.js} +2 -18
- package/dist/metrics/index.d.ts +9 -0
- package/dist/metrics/index.js +6 -0
- package/dist/metrics/runtime.d.ts +25 -0
- package/dist/metrics/runtime.js +28 -0
- package/dist/metrics/snapshot/flush.d.ts +6 -0
- package/dist/{metrics-output.js → metrics/snapshot/flush.js} +6 -290
- package/dist/metrics/snapshot/merge.d.ts +9 -0
- package/dist/metrics/snapshot/merge.js +282 -0
- package/dist/{metrics-steps.d.ts → metrics/snapshot/steps.d.ts} +1 -1
- package/dist/{metrics-steps.js → metrics/snapshot/steps.js} +1 -1
- package/dist/{metrics-types.d.ts → metrics/types.d.ts} +3 -81
- package/dist/metrics/types.js +20 -0
- package/dist/plugin.d.ts +22 -0
- package/dist/plugin.js +49 -0
- package/dist/{logger.d.ts → shared/log.d.ts} +2 -0
- package/dist/{logger.js → shared/log.js} +9 -2
- package/package.json +8 -4
- package/dist/backfill-cli.d.ts +0 -1
- package/dist/backfill-cli.js +0 -74
- package/dist/backfill.d.ts +0 -58
- package/dist/backfill.js +0 -550
- package/dist/dirs.d.ts +0 -34
- package/dist/metrics-engine.d.ts +0 -37
- package/dist/metrics-output.d.ts +0 -11
- package/dist/summary-store.d.ts +0 -150
- package/dist/summary-store.js +0 -560
- /package/dist/{compile-analyzer.d.ts → metrics/analysis/hvigor.d.ts} +0 -0
- /package/dist/{compile-analyzer.js → metrics/analysis/hvigor.js} +0 -0
package/dist/backfill.js
DELETED
|
@@ -1,550 +0,0 @@
|
|
|
1
|
-
import * as fs from "fs";
|
|
2
|
-
import * as path from "path";
|
|
3
|
-
import * as os from "os";
|
|
4
|
-
import { getSummaryFile } from "./dirs.js";
|
|
5
|
-
import { upsertSessionSnapshot } from "./summary-store.js";
|
|
6
|
-
// ─── 工具函数 ────────────────────────────────────────────────────────────────
|
|
7
|
-
// 状态文件不带 .json 扩展名,避免被会话快照扫描器(session-viewer 等)误读
|
|
8
|
-
const STATE_FILE = ".backfill-state";
|
|
9
|
-
function zeroTokens() {
|
|
10
|
-
return { input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
|
|
11
|
-
}
|
|
12
|
-
function addTokens(a, b) {
|
|
13
|
-
return {
|
|
14
|
-
input: a.input + b.input,
|
|
15
|
-
output: a.output + b.output,
|
|
16
|
-
reasoning: a.reasoning + b.reasoning,
|
|
17
|
-
cacheRead: a.cacheRead + b.cacheRead,
|
|
18
|
-
cacheWrite: a.cacheWrite + b.cacheWrite,
|
|
19
|
-
total: a.total + b.total,
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
/** message.data.tokens 结构 {input,output,reasoning,cache:{read,write}} → 扁平 TokenUsage */
|
|
23
|
-
function msgTokens(raw) {
|
|
24
|
-
const t = (raw ?? {});
|
|
25
|
-
const cache = (t.cache ?? {});
|
|
26
|
-
const n = (v) => (typeof v === "number" && Number.isFinite(v) ? v : 0);
|
|
27
|
-
const usage = {
|
|
28
|
-
input: n(t.input), output: n(t.output), reasoning: n(t.reasoning),
|
|
29
|
-
cacheRead: n(cache.read), cacheWrite: n(cache.write), total: 0,
|
|
30
|
-
};
|
|
31
|
-
usage.total = usage.input + usage.output + usage.reasoning;
|
|
32
|
-
return usage;
|
|
33
|
-
}
|
|
34
|
-
function sessionTokens(row) {
|
|
35
|
-
const n = (v) => (typeof v === "number" && Number.isFinite(v) ? v : 0);
|
|
36
|
-
const usage = {
|
|
37
|
-
input: n(row.tokens_input), output: n(row.tokens_output), reasoning: n(row.tokens_reasoning),
|
|
38
|
-
cacheRead: n(row.tokens_cache_read), cacheWrite: n(row.tokens_cache_write), total: 0,
|
|
39
|
-
};
|
|
40
|
-
usage.total = usage.input + usage.output + usage.reasoning;
|
|
41
|
-
return usage;
|
|
42
|
-
}
|
|
43
|
-
/** 目录前缀匹配(统一正斜杠 + 大小写不敏感,兼容 Windows 反斜杠配置) */
|
|
44
|
-
function dirMatches(directory, prefixes) {
|
|
45
|
-
if (!directory || !prefixes || prefixes.length === 0)
|
|
46
|
-
return false;
|
|
47
|
-
const dir = directory.replace(/\\/g, "/").toLowerCase();
|
|
48
|
-
return prefixes.some((p) => dir.startsWith(p.replace(/\\/g, "/").toLowerCase()));
|
|
49
|
-
}
|
|
50
|
-
function parseModelDisplay(modelJson) {
|
|
51
|
-
if (!modelJson)
|
|
52
|
-
return "";
|
|
53
|
-
try {
|
|
54
|
-
const m = JSON.parse(modelJson);
|
|
55
|
-
// 只存模型名(如 deepseek-v4-flash),不带 provider 前缀(与 live 的 model.id 口径一致)
|
|
56
|
-
return m.id ?? modelJson;
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
return modelJson;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
function readWatermark(metricsDir) {
|
|
63
|
-
try {
|
|
64
|
-
const raw = fs.readFileSync(path.join(metricsDir, STATE_FILE), "utf-8");
|
|
65
|
-
const v = JSON.parse(raw).watermark;
|
|
66
|
-
return typeof v === "number" ? v : 0;
|
|
67
|
-
}
|
|
68
|
-
catch {
|
|
69
|
-
return 0;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
function writeWatermark(metricsDir, watermark) {
|
|
73
|
-
try {
|
|
74
|
-
fs.mkdirSync(metricsDir, { recursive: true });
|
|
75
|
-
fs.writeFileSync(path.join(metricsDir, STATE_FILE), JSON.stringify({ watermark }, null, 2));
|
|
76
|
-
}
|
|
77
|
-
catch { }
|
|
78
|
-
}
|
|
79
|
-
/** 从 message/part 行重建单会话(或子会话)的 steps/skills/工具统计 */
|
|
80
|
-
function buildFromRows(messages, parts) {
|
|
81
|
-
const partsByMsg = new Map();
|
|
82
|
-
for (const p of parts) {
|
|
83
|
-
if (!p.message_id)
|
|
84
|
-
continue;
|
|
85
|
-
const list = partsByMsg.get(p.message_id);
|
|
86
|
-
if (list)
|
|
87
|
-
list.push(p);
|
|
88
|
-
else
|
|
89
|
-
partsByMsg.set(p.message_id, [p]);
|
|
90
|
-
}
|
|
91
|
-
const steps = [];
|
|
92
|
-
const skills = [];
|
|
93
|
-
let responseLength = 0;
|
|
94
|
-
// 内部累加器(含 totalDuration),输出时换算为 avgDuration
|
|
95
|
-
const accDistribution = new Map();
|
|
96
|
-
let totalCalls = 0;
|
|
97
|
-
let totalErrors = 0;
|
|
98
|
-
let slowest = null;
|
|
99
|
-
for (const msg of messages) {
|
|
100
|
-
let md;
|
|
101
|
-
try {
|
|
102
|
-
md = JSON.parse(msg.data);
|
|
103
|
-
}
|
|
104
|
-
catch {
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
const role = md.role;
|
|
108
|
-
if (role === "assistant") {
|
|
109
|
-
const msgParts = (partsByMsg.get(msg.id) ?? []);
|
|
110
|
-
let text = "";
|
|
111
|
-
let reasoning = "";
|
|
112
|
-
const tools = [];
|
|
113
|
-
for (const p of msgParts) {
|
|
114
|
-
let pd;
|
|
115
|
-
try {
|
|
116
|
-
pd = JSON.parse(p.data);
|
|
117
|
-
}
|
|
118
|
-
catch {
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
const state = (pd.state ?? {});
|
|
122
|
-
const status = state.status;
|
|
123
|
-
if (pd.type === "text") {
|
|
124
|
-
text += pd.text ?? "";
|
|
125
|
-
}
|
|
126
|
-
else if (pd.type === "reasoning") {
|
|
127
|
-
reasoning += pd.text ?? "";
|
|
128
|
-
}
|
|
129
|
-
else if (pd.type === "tool" && status && status !== "running") {
|
|
130
|
-
const toolName = pd.tool ?? "unknown";
|
|
131
|
-
const time = (state.time ?? {});
|
|
132
|
-
const start = typeof time.start === "number" ? time.start : p.time_created;
|
|
133
|
-
const end = typeof time.end === "number" ? time.end : p.time_updated;
|
|
134
|
-
const durationMs = end > start ? end - start : 0;
|
|
135
|
-
const output = state.output ?? state.raw ?? "";
|
|
136
|
-
tools.push({
|
|
137
|
-
tool: toolName,
|
|
138
|
-
callID: pd.callID ?? p.id,
|
|
139
|
-
status: status === "error" ? "error" : "completed",
|
|
140
|
-
input: (state.input ?? pd.input ?? {}),
|
|
141
|
-
output,
|
|
142
|
-
durationMs,
|
|
143
|
-
});
|
|
144
|
-
// 工具分布统计
|
|
145
|
-
const stat = accDistribution.get(toolName) ?? { calls: 0, errors: 0, totalDuration: 0, maxDuration: 0 };
|
|
146
|
-
stat.calls++;
|
|
147
|
-
if (status === "error") {
|
|
148
|
-
stat.errors++;
|
|
149
|
-
totalErrors++;
|
|
150
|
-
}
|
|
151
|
-
stat.totalDuration += durationMs;
|
|
152
|
-
stat.maxDuration = Math.max(stat.maxDuration, durationMs);
|
|
153
|
-
accDistribution.set(toolName, stat);
|
|
154
|
-
totalCalls++;
|
|
155
|
-
if (!slowest || durationMs > slowest.duration)
|
|
156
|
-
slowest = { tool: toolName, duration: durationMs };
|
|
157
|
-
// skill 调用(与运行时 collectSkills 同口径:tool==='skill',名称取 state.input.name)
|
|
158
|
-
if (toolName === "skill") {
|
|
159
|
-
const input = (state.input ?? {});
|
|
160
|
-
skills.push({
|
|
161
|
-
skillName: input.name ?? "skill",
|
|
162
|
-
status: status === "error" ? "failed" : "completed",
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
const tokens = msgTokens(md.tokens);
|
|
168
|
-
const hasContent = tokens.total > 0 || tools.length > 0 || text.length > 0;
|
|
169
|
-
if (hasContent) {
|
|
170
|
-
responseLength += text.length;
|
|
171
|
-
steps.push({
|
|
172
|
-
index: steps.length,
|
|
173
|
-
startTime: msg.time_created,
|
|
174
|
-
endTime: msg.time_updated > msg.time_created ? msg.time_updated : msg.time_created,
|
|
175
|
-
durationMs: msg.time_updated > msg.time_created ? msg.time_updated - msg.time_created : 0,
|
|
176
|
-
tokens,
|
|
177
|
-
cost: typeof md.cost === "number" ? md.cost : 0,
|
|
178
|
-
tools,
|
|
179
|
-
textLength: text.length,
|
|
180
|
-
text,
|
|
181
|
-
reasoning,
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
// 汇总分布(avg 由 totalDuration 换算)
|
|
187
|
-
const dist = {};
|
|
188
|
-
for (const [tool, s] of accDistribution) {
|
|
189
|
-
dist[tool] = {
|
|
190
|
-
calls: s.calls, errors: s.errors,
|
|
191
|
-
avgDuration: s.calls > 0 ? s.totalDuration / s.calls : 0,
|
|
192
|
-
maxDuration: s.maxDuration,
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
return {
|
|
196
|
-
steps,
|
|
197
|
-
skills,
|
|
198
|
-
responseLength,
|
|
199
|
-
toolStats: {
|
|
200
|
-
totalCalls,
|
|
201
|
-
invalidCalls: 0,
|
|
202
|
-
successRate: totalCalls > 0 ? (totalCalls - totalErrors) / totalCalls : 0,
|
|
203
|
-
distribution: dist,
|
|
204
|
-
slowestCall: slowest,
|
|
205
|
-
},
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
function emptyPlanning() {
|
|
209
|
-
return {
|
|
210
|
-
planningRounds: 0, totalPlanningDuration: 0, avgPlanningDuration: 0,
|
|
211
|
-
totalTasks: 0, completedTasks: 0, inProgressTasks: 0, pendingTasks: 0,
|
|
212
|
-
completionRate: 0, priorityDistribution: {}, calls: [],
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
/** 从消息时间线按真实对话轮次生成 rounds:user 消息开轮,其后连续 assistant 消息归属该轮 */
|
|
216
|
-
function roundsFromTimeline(messages, parts) {
|
|
217
|
-
const partsByMsg = new Map();
|
|
218
|
-
for (const p of parts) {
|
|
219
|
-
if (!p.message_id)
|
|
220
|
-
continue;
|
|
221
|
-
const list = partsByMsg.get(p.message_id);
|
|
222
|
-
if (list)
|
|
223
|
-
list.push(p);
|
|
224
|
-
else
|
|
225
|
-
partsByMsg.set(p.message_id, [p]);
|
|
226
|
-
}
|
|
227
|
-
const zeroTokens = () => ({ input: 0, output: 0, reasoning: 0, cacheRead: 0, cacheWrite: 0, total: 0 });
|
|
228
|
-
const rounds = [];
|
|
229
|
-
let current = null;
|
|
230
|
-
const pushRound = () => {
|
|
231
|
-
// 与 live 同口径:空轮(无 tokens 且无工具调用)不生成快照
|
|
232
|
-
if (current && (current.tokens.total > 0 || current.toolCalls > 0))
|
|
233
|
-
rounds.push(current);
|
|
234
|
-
current = null;
|
|
235
|
-
};
|
|
236
|
-
const ordered = [...messages].sort((a, b) => a.time_created - b.time_created);
|
|
237
|
-
for (const msg of ordered) {
|
|
238
|
-
let md;
|
|
239
|
-
try {
|
|
240
|
-
md = JSON.parse(msg.data);
|
|
241
|
-
}
|
|
242
|
-
catch {
|
|
243
|
-
continue;
|
|
244
|
-
}
|
|
245
|
-
const role = md.role;
|
|
246
|
-
if (role === "user") {
|
|
247
|
-
pushRound();
|
|
248
|
-
const texts = [];
|
|
249
|
-
for (const p of partsByMsg.get(msg.id) ?? []) {
|
|
250
|
-
try {
|
|
251
|
-
const pd = JSON.parse(p.data);
|
|
252
|
-
if (pd.type === "text" && typeof pd.text === "string" && pd.text.length > 0)
|
|
253
|
-
texts.push(pd.text);
|
|
254
|
-
}
|
|
255
|
-
catch { /* 跳过坏行 */ }
|
|
256
|
-
}
|
|
257
|
-
current = {
|
|
258
|
-
userMessage: texts.length > 0 ? texts : undefined,
|
|
259
|
-
roundStart: msg.time_created,
|
|
260
|
-
firstTextAt: 0,
|
|
261
|
-
tokens: zeroTokens(),
|
|
262
|
-
toolCalls: 0,
|
|
263
|
-
errors: 0,
|
|
264
|
-
duration: 0,
|
|
265
|
-
};
|
|
266
|
-
continue;
|
|
267
|
-
}
|
|
268
|
-
if (role !== "assistant")
|
|
269
|
-
continue;
|
|
270
|
-
if (!current) {
|
|
271
|
-
current = { userMessage: undefined, roundStart: msg.time_created, firstTextAt: 0, tokens: zeroTokens(), toolCalls: 0, errors: 0, duration: 0 };
|
|
272
|
-
}
|
|
273
|
-
const t = msgTokens(md.tokens);
|
|
274
|
-
current.tokens.input += t.input;
|
|
275
|
-
current.tokens.output += t.output;
|
|
276
|
-
current.tokens.reasoning += t.reasoning;
|
|
277
|
-
current.tokens.cacheRead += t.cacheRead;
|
|
278
|
-
current.tokens.cacheWrite += t.cacheWrite;
|
|
279
|
-
current.tokens.total += t.total;
|
|
280
|
-
for (const p of partsByMsg.get(msg.id) ?? []) {
|
|
281
|
-
try {
|
|
282
|
-
const pd = JSON.parse(p.data);
|
|
283
|
-
if (pd.type === "text") {
|
|
284
|
-
if (!current.firstTextAt)
|
|
285
|
-
current.firstTextAt = p.time_created;
|
|
286
|
-
}
|
|
287
|
-
else if (pd.type === "tool") {
|
|
288
|
-
const status = (pd.state ?? {}).status;
|
|
289
|
-
if (status && status !== "running") {
|
|
290
|
-
current.toolCalls++;
|
|
291
|
-
if (status === "error")
|
|
292
|
-
current.errors++;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
catch { /* 跳过坏行 */ }
|
|
297
|
-
}
|
|
298
|
-
current.duration += msg.time_updated > msg.time_created ? msg.time_updated - msg.time_created : 0;
|
|
299
|
-
}
|
|
300
|
-
pushRound();
|
|
301
|
-
return rounds.map((r, i) => ({
|
|
302
|
-
roundIndex: i,
|
|
303
|
-
duration: r.duration,
|
|
304
|
-
firstTokenLatency: r.firstTextAt > r.roundStart ? r.firstTextAt - r.roundStart : 0,
|
|
305
|
-
tokens: { ...r.tokens },
|
|
306
|
-
toolCalls: r.toolCalls,
|
|
307
|
-
errors: r.errors,
|
|
308
|
-
userMessage: r.userMessage,
|
|
309
|
-
}));
|
|
310
|
-
}
|
|
311
|
-
// ─── 主流程 ──────────────────────────────────────────────────────────────────
|
|
312
|
-
/** 从 opencode.db 回填历史会话统计:按归属规则路由写入各引擎目录(快照带 source:"backfill")。 */
|
|
313
|
-
export async function backfillFromOpencode(opts) {
|
|
314
|
-
const dbPath = opts.dbPath ?? path.join(os.homedir(), ".local", "share", "opencode", "opencode.db");
|
|
315
|
-
const skipExisting = opts.skipExisting !== false;
|
|
316
|
-
const { DatabaseSync } = await import("node:sqlite");
|
|
317
|
-
const db = new DatabaseSync(dbPath, { readOnly: true });
|
|
318
|
-
try {
|
|
319
|
-
const sessions = db.prepare("SELECT id,parent_id,directory,title,agent,model,cost,tokens_input,tokens_output,tokens_reasoning,tokens_cache_read,tokens_cache_write,time_created,time_updated,time_compacting FROM session ORDER BY time_created").all();
|
|
320
|
-
const childrenByParent = new Map();
|
|
321
|
-
for (const s of sessions) {
|
|
322
|
-
if (s.parent_id) {
|
|
323
|
-
const list = childrenByParent.get(s.parent_id);
|
|
324
|
-
if (list)
|
|
325
|
-
list.push(s);
|
|
326
|
-
else
|
|
327
|
-
childrenByParent.set(s.parent_id, [s]);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
const msgCountStmt = db.prepare("SELECT COUNT(*) AS n FROM message WHERE session_id = ?");
|
|
331
|
-
const agentStmt = db.prepare("SELECT data FROM message WHERE session_id = ? ORDER BY time_created");
|
|
332
|
-
const msgStmt = db.prepare("SELECT id,time_created,time_updated,data FROM message WHERE session_id = ? ORDER BY time_created");
|
|
333
|
-
const partStmt = db.prepare("SELECT id,message_id,time_created,time_updated,data FROM part WHERE session_id = ? ORDER BY time_created");
|
|
334
|
-
/** agent 归属链:session.agent → 首条含 agent 的 message */
|
|
335
|
-
function resolveAgent(row) {
|
|
336
|
-
if (row.agent)
|
|
337
|
-
return row.agent;
|
|
338
|
-
try {
|
|
339
|
-
for (const m of agentStmt.all(row.id)) {
|
|
340
|
-
try {
|
|
341
|
-
const d = JSON.parse(m.data);
|
|
342
|
-
if (typeof d.agent === "string" && d.agent)
|
|
343
|
-
return d.agent;
|
|
344
|
-
}
|
|
345
|
-
catch { }
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
catch { }
|
|
349
|
-
return null;
|
|
350
|
-
}
|
|
351
|
-
function attribute(row, agent) {
|
|
352
|
-
for (const e of opts.engines) {
|
|
353
|
-
if (e.agents && agent && e.agents.includes(agent))
|
|
354
|
-
return e;
|
|
355
|
-
if (dirMatches(row.directory, e.cwdPrefixes))
|
|
356
|
-
return e;
|
|
357
|
-
}
|
|
358
|
-
return opts.fallback ? "fallback" : null;
|
|
359
|
-
}
|
|
360
|
-
const stats = new Map();
|
|
361
|
-
const statOf = (label, dir) => {
|
|
362
|
-
let s = stats.get(label + "\u0000" + dir);
|
|
363
|
-
if (!s) {
|
|
364
|
-
s = { label, metricsDir: dir, written: 0, skippedExisting: 0, skippedEmpty: 0 };
|
|
365
|
-
stats.set(label + "\u0000" + dir, s);
|
|
366
|
-
}
|
|
367
|
-
return s;
|
|
368
|
-
};
|
|
369
|
-
for (const e of opts.engines)
|
|
370
|
-
statOf(e.label, e.metricsDir);
|
|
371
|
-
if (opts.fallback)
|
|
372
|
-
statOf("fallback", opts.fallback.metricsDir);
|
|
373
|
-
const watermarks = new Map();
|
|
374
|
-
const newWatermarks = new Map();
|
|
375
|
-
const wmOf = (dir) => {
|
|
376
|
-
let w = watermarks.get(dir);
|
|
377
|
-
if (w === undefined) {
|
|
378
|
-
w = opts.incremental ? readWatermark(dir) : 0;
|
|
379
|
-
watermarks.set(dir, w);
|
|
380
|
-
}
|
|
381
|
-
return w;
|
|
382
|
-
};
|
|
383
|
-
let skippedByFilter = 0;
|
|
384
|
-
let unattributed = 0;
|
|
385
|
-
let total = 0;
|
|
386
|
-
for (const row of sessions) {
|
|
387
|
-
if (row.parent_id !== null)
|
|
388
|
-
continue; // 子会话并入父快照
|
|
389
|
-
total++;
|
|
390
|
-
const f = opts.filter;
|
|
391
|
-
if (f) {
|
|
392
|
-
if (f.sessionIds && !f.sessionIds.includes(row.id)) {
|
|
393
|
-
skippedByFilter++;
|
|
394
|
-
continue;
|
|
395
|
-
}
|
|
396
|
-
if (typeof f.since === "number" && row.time_created <= f.since) {
|
|
397
|
-
skippedByFilter++;
|
|
398
|
-
continue;
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
// cwdPrefixes 前置(纯字符串比较零开销):不匹配行先跳过,避免为大批
|
|
402
|
-
// 目录外会话支付 resolveAgent 的全消息扫描代价(agent 留到 attribute 前解析)
|
|
403
|
-
if (f?.cwdPrefixes && f.cwdPrefixes.length > 0 && !dirMatches(row.directory, f.cwdPrefixes)) {
|
|
404
|
-
skippedByFilter++;
|
|
405
|
-
continue;
|
|
406
|
-
}
|
|
407
|
-
const agent = resolveAgent(row);
|
|
408
|
-
if (f?.agents && f.agents.length > 0 && (!agent || !f.agents.includes(agent))) {
|
|
409
|
-
skippedByFilter++;
|
|
410
|
-
continue;
|
|
411
|
-
}
|
|
412
|
-
// 空会话(父+子均无消息)无回填价值
|
|
413
|
-
const parentMsgs = msgCountStmt.get(row.id).n;
|
|
414
|
-
const children = childrenByParent.get(row.id) ?? [];
|
|
415
|
-
const childMsgs = children.reduce((sum, c) => sum + msgCountStmt.get(c.id).n, 0);
|
|
416
|
-
const target = attribute(row, agent);
|
|
417
|
-
if (!target) {
|
|
418
|
-
unattributed++;
|
|
419
|
-
continue;
|
|
420
|
-
}
|
|
421
|
-
const label = target === "fallback" ? "fallback" : target.label;
|
|
422
|
-
const metricsDir = target === "fallback" ? opts.fallback.metricsDir : target.metricsDir;
|
|
423
|
-
// 摘要库落点:rule/fallback 显式指定,否则全局共享库(与 live flush 同库,多宿主聚合)
|
|
424
|
-
const summaryFile = (target === "fallback" ? opts.fallback.summaryFile : target.summaryFile) ?? getSummaryFile();
|
|
425
|
-
const stat = statOf(label, metricsDir);
|
|
426
|
-
if (parentMsgs === 0 && childMsgs === 0) {
|
|
427
|
-
stat.skippedEmpty++;
|
|
428
|
-
continue;
|
|
429
|
-
}
|
|
430
|
-
// 增量水位(按目标目录)
|
|
431
|
-
if (opts.incremental && row.time_created <= wmOf(metricsDir))
|
|
432
|
-
continue;
|
|
433
|
-
// live 快照优先,不覆盖;force 全量重跑时仅保护 source:"live"
|
|
434
|
-
//(backfill 源与无标记 legacy 均可覆盖——标记缺失视为历史产物,需可重建以治愈存量摘要行)
|
|
435
|
-
{
|
|
436
|
-
const snapPath = path.join(metricsDir, `${row.id}.json`);
|
|
437
|
-
if (fs.existsSync(snapPath)) {
|
|
438
|
-
let protectedLive = false;
|
|
439
|
-
if (!skipExisting) {
|
|
440
|
-
try {
|
|
441
|
-
protectedLive = JSON.parse(fs.readFileSync(snapPath, "utf-8")).source === "live";
|
|
442
|
-
}
|
|
443
|
-
catch { /* 坏文件视为可覆盖 */ }
|
|
444
|
-
}
|
|
445
|
-
if (skipExisting || protectedLive) {
|
|
446
|
-
stat.skippedExisting++;
|
|
447
|
-
continue;
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
// 构建快照
|
|
452
|
-
const parentMessages = msgStmt.all(row.id);
|
|
453
|
-
const parentParts = partStmt.all(row.id);
|
|
454
|
-
const built = buildFromRows(parentMessages, parentParts);
|
|
455
|
-
const childrenBuilt = [];
|
|
456
|
-
for (const c of children) {
|
|
457
|
-
const cMsgs = msgStmt.all(c.id);
|
|
458
|
-
if (cMsgs.length === 0)
|
|
459
|
-
continue;
|
|
460
|
-
childrenBuilt.push({ row: c, built: buildFromRows(cMsgs, partStmt.all(c.id)) });
|
|
461
|
-
}
|
|
462
|
-
let tokens = sessionTokens(row);
|
|
463
|
-
for (const cb of childrenBuilt)
|
|
464
|
-
tokens = addTokens(tokens, sessionTokens(cb.row));
|
|
465
|
-
const denom = tokens.input + tokens.cacheRead;
|
|
466
|
-
const stepCount = built.steps.length;
|
|
467
|
-
const output = {
|
|
468
|
-
sessionId: row.id,
|
|
469
|
-
startTime: row.time_created,
|
|
470
|
-
endTime: row.time_updated > row.time_created ? row.time_updated : row.time_created,
|
|
471
|
-
duration: row.time_updated > row.time_created ? row.time_updated - row.time_created : 0,
|
|
472
|
-
systemPrompts: {},
|
|
473
|
-
rounds: roundsFromTimeline(parentMessages, parentParts),
|
|
474
|
-
tokens: {
|
|
475
|
-
...tokens,
|
|
476
|
-
cacheHitRate: denom > 0 ? tokens.cacheRead / denom : 0,
|
|
477
|
-
avgTokensPerStep: stepCount > 0 ? tokens.total / stepCount : 0,
|
|
478
|
-
},
|
|
479
|
-
tools: {
|
|
480
|
-
...built.toolStats,
|
|
481
|
-
slowestCall: built.toolStats.slowestCall ?? { tool: "", duration: 0 },
|
|
482
|
-
},
|
|
483
|
-
compactions: row.time_compacting ? 1 : 0,
|
|
484
|
-
anomaly: { triggered: false, events: [] },
|
|
485
|
-
stages: [],
|
|
486
|
-
header: {
|
|
487
|
-
sessionId: row.id,
|
|
488
|
-
workingDirectory: row.directory ?? "",
|
|
489
|
-
startTime: new Date(row.time_created).toISOString(),
|
|
490
|
-
model: parseModelDisplay(row.model),
|
|
491
|
-
agent: agent ?? "",
|
|
492
|
-
agentSwitches: 0,
|
|
493
|
-
agentUsage: agent ? { [agent]: 0 } : {},
|
|
494
|
-
modelSwitches: 0,
|
|
495
|
-
modelTokenDistribution: {},
|
|
496
|
-
},
|
|
497
|
-
codeStats: {
|
|
498
|
-
etsLines: 0, buildSuccess: false, fixCompileCount: 0, totalCompileErrors: 0,
|
|
499
|
-
firstBuildPerRound: [], firstBuildPassRate: 0, errorCodes: [],
|
|
500
|
-
warnings: { total: 0, byType: {}, entries: [] },
|
|
501
|
-
fixCycles: { successRate: 0, avgAttempts: 0, cycles: [] },
|
|
502
|
-
moduleTimings: [],
|
|
503
|
-
},
|
|
504
|
-
responseLength: built.responseLength,
|
|
505
|
-
skills: built.skills,
|
|
506
|
-
skillSearches: [],
|
|
507
|
-
planning: emptyPlanning(),
|
|
508
|
-
steps: built.steps,
|
|
509
|
-
subagents: childrenBuilt.map((cb) => ({
|
|
510
|
-
sessionId: cb.row.id,
|
|
511
|
-
agent: cb.row.agent ?? "",
|
|
512
|
-
title: cb.row.title ?? "",
|
|
513
|
-
steps: cb.built.steps,
|
|
514
|
-
tokens: sessionTokens(cb.row),
|
|
515
|
-
tools: {
|
|
516
|
-
...cb.built.toolStats,
|
|
517
|
-
slowestCall: cb.built.toolStats.slowestCall ?? { tool: "", duration: 0 },
|
|
518
|
-
},
|
|
519
|
-
})),
|
|
520
|
-
source: "backfill",
|
|
521
|
-
};
|
|
522
|
-
if (!opts.dryRun) {
|
|
523
|
-
fs.mkdirSync(metricsDir, { recursive: true });
|
|
524
|
-
const filePath = path.join(metricsDir, `${row.id}.json`);
|
|
525
|
-
const tmpPath = filePath + ".tmp";
|
|
526
|
-
fs.writeFileSync(tmpPath, JSON.stringify(output, null, 2));
|
|
527
|
-
fs.renameSync(tmpPath, filePath);
|
|
528
|
-
// 摘要/详情双表联动入库(增量 soft:live 已有非零数据不覆盖;force 重跑强制刷新)
|
|
529
|
-
upsertSessionSnapshot(summaryFile, output, filePath, skipExisting ? "soft" : "force");
|
|
530
|
-
}
|
|
531
|
-
stat.written++;
|
|
532
|
-
const prev = newWatermarks.get(metricsDir) ?? 0;
|
|
533
|
-
if (row.time_created > prev)
|
|
534
|
-
newWatermarks.set(metricsDir, row.time_created);
|
|
535
|
-
opts.onSessionBackfilled?.(row.id, label, metricsDir);
|
|
536
|
-
}
|
|
537
|
-
if (!opts.dryRun) {
|
|
538
|
-
for (const [dir, w] of newWatermarks) {
|
|
539
|
-
const prev = opts.incremental ? wmOf(dir) : 0;
|
|
540
|
-
writeWatermark(dir, Math.max(prev, w));
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
const perEngine = opts.engines.map((e) => stats.get(e.label + "\u0000" + e.metricsDir)).filter(Boolean);
|
|
544
|
-
const fb = opts.fallback ? stats.get("fallback\u0000" + opts.fallback.metricsDir) ?? null : null;
|
|
545
|
-
return { perEngine, fallback: fb, skippedByFilter, unattributed, total };
|
|
546
|
-
}
|
|
547
|
-
finally {
|
|
548
|
-
db.close();
|
|
549
|
-
}
|
|
550
|
-
}
|
package/dist/dirs.d.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/** metrics/events 写入目录集。host 可自定义;默认按应用名 env paths(与 session-viewer-plugin 约定一致)。 */
|
|
2
|
-
export interface MetricsDirs {
|
|
3
|
-
/** 会话快照 JSON 目录(<sessionId>.json 直接落这里;各宿主必须隔离,避免多写者冲突) */
|
|
4
|
-
metricsDir: string;
|
|
5
|
-
/** events 逐会话日志目录 */
|
|
6
|
-
eventsDir: string;
|
|
7
|
-
/** 运行日志文件(可选;logger 无文件权限时回退 console) */
|
|
8
|
-
logFile: string;
|
|
9
|
-
/**
|
|
10
|
-
* 摘要/详情双表 sqlite 落点。缺省为包级全局路径:本机多宿主共享同一个 summary.db,
|
|
11
|
-
* 查询一处即可获得全部宿主的会话摘要。显式传入本字段的宿主可获得独立隔离库。
|
|
12
|
-
* (快照 JSON 目录仍按宿主隔离,summary 双表在库层聚合。)
|
|
13
|
-
*/
|
|
14
|
-
summaryFile: string;
|
|
15
|
-
}
|
|
16
|
-
export declare function defaultDirs(appName?: string): MetricsDirs;
|
|
17
|
-
/**
|
|
18
|
-
* 供 host 设定进程级默认目录(未显式传 dirs 的引擎/事件记录器使用)。
|
|
19
|
-
* 注意:多引擎场景请优先在 createMetricsEngine/createEventLogger 实例级传 dirs,
|
|
20
|
-
* 进程级配置仅作默认兜底,避免多宿主互相覆盖。
|
|
21
|
-
* summaryFile 未显式配置时保持包级全局路径(跨宿主聚合语义)。
|
|
22
|
-
*/
|
|
23
|
-
export declare function configureDirs(dirs: Partial<MetricsDirs>): void;
|
|
24
|
-
/** 当前进程级默认目录(defaultDirs 与 configureDirs 的合并结果)。 */
|
|
25
|
-
export declare function getDirs(): MetricsDirs;
|
|
26
|
-
/**
|
|
27
|
-
* 解析实例目录:defaultDirs → 进程级默认(configureDirs)→ 实例传入,三层合并。
|
|
28
|
-
* summaryFile 不随 metricsDir 派生:缺省恒为全局共享库(实例/进程级显式指定才能改写)。
|
|
29
|
-
*/
|
|
30
|
-
export declare function resolveDirs(partial?: Partial<MetricsDirs>): MetricsDirs;
|
|
31
|
-
export declare function getMetricsDir(): string;
|
|
32
|
-
export declare function getEventsDir(): string;
|
|
33
|
-
export declare function getLogFile(): string;
|
|
34
|
-
export declare function getSummaryFile(): string;
|
package/dist/metrics-engine.d.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import type { MetricsEngine, MetricsOutput } from "./metrics-types.js";
|
|
2
|
-
import type { MetricsDirs } from "./dirs.js";
|
|
3
|
-
export type { TokenUsage, ToolStats, StageType, StageInfo, MessageEntry, ToolCallEntry, SessionMeta, CompileStats, SkillCallEntry, SkillSearchEntry, StepData, RoundSnapshot, SubAgentOutput, MetricsOutput, MetricsEngine, } from "./metrics-types.js";
|
|
4
|
-
export type MetricsScopeMode = "all" | "none" | "declared" | "cwd";
|
|
5
|
-
export interface MetricsScope {
|
|
6
|
-
mode: MetricsScopeMode;
|
|
7
|
-
/** 交叉生效的 agent 白名单:非空时仅统计这些 agent 名的会话(session.created/updated 的 info.agent) */
|
|
8
|
-
agents?: string[];
|
|
9
|
-
/** mode==='cwd' 时生效:仅统计工作目录命中这些前缀的会话 */
|
|
10
|
-
cwdPrefixes?: string[];
|
|
11
|
-
}
|
|
12
|
-
export interface MetricsEngineOptions {
|
|
13
|
-
enabled?: boolean;
|
|
14
|
-
scope?: MetricsScope;
|
|
15
|
-
/** 本引擎专属输出目录(多引擎各自独立);缺省回退进程级默认(configureDirs),构造时解析固化 */
|
|
16
|
-
dirs?: Partial<MetricsDirs>;
|
|
17
|
-
/** flush 会话快照后回调(宿主可在此把快照写入 DB/上报,替代或补充文件落盘) */
|
|
18
|
-
onFlush?: (sessionId: string, snapshot: MetricsOutput) => void;
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* 创建指标引擎。
|
|
22
|
-
* 兼容旧布尔入参:true = scope all,false = scope none。
|
|
23
|
-
* 统计范围 = 策略 + 宿主声明(claimSession)的交集判定:
|
|
24
|
-
* - mode all —— 全部(配合 agents 即只统计白名单 agent)
|
|
25
|
-
* - mode none —— 默认不统计;宿主 claimSession 才统计
|
|
26
|
-
* - mode declared—— 仅宿主 claimSession 的会话(或 agents 白名单命中)
|
|
27
|
-
* - mode cwd —— 仅工作目录命中 cwdPrefixes
|
|
28
|
-
* 子会话(parentID 归属已统计父会话)自动随父会话统计。
|
|
29
|
-
*/
|
|
30
|
-
export declare function createMetricsEngine(opts?: MetricsEngineOptions | boolean): MetricsEngine & {
|
|
31
|
-
claimSession: (sessionId: string, meta?: {
|
|
32
|
-
agent?: string;
|
|
33
|
-
}) => void;
|
|
34
|
-
scopeOf: (sessionId: string) => boolean;
|
|
35
|
-
/** 归属判定(供 event-logger 等共用同一 scope 语义;与 ingest 内部判定一致) */
|
|
36
|
-
shouldTrack: (sessionId: string, eventType: string, props: Record<string, unknown> | undefined) => boolean;
|
|
37
|
-
};
|
package/dist/metrics-output.d.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { MetricsDirs } from "./dirs.js";
|
|
2
|
-
import type { SessionMetricsState, MetricsOutput } from "./metrics-types.js";
|
|
3
|
-
declare function flushMetrics(sessionId: string, state: SessionMetricsState, now: number, dirs?: Partial<MetricsDirs>): MetricsOutput;
|
|
4
|
-
declare function handleSessionIdle(state: SessionMetricsState, sessionId: string, dirs?: Partial<MetricsDirs>): MetricsOutput | undefined;
|
|
5
|
-
declare function mergeChildMetrics(parent: SessionMetricsState, child: SessionMetricsState, meta: {
|
|
6
|
-
childId: string;
|
|
7
|
-
agentName: string;
|
|
8
|
-
title: string;
|
|
9
|
-
}): void;
|
|
10
|
-
export declare function mergeMetricsOutput(existing: MetricsOutput | null, fresh: MetricsOutput): MetricsOutput;
|
|
11
|
-
export { flushMetrics, handleSessionIdle, mergeChildMetrics };
|