opencode-metrics-plugin 0.3.5 → 0.4.2

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.
@@ -353,11 +353,17 @@ function dedupePaths(paths) {
353
353
  * 启动清扫暂存目录顶层:崩溃遗留的完整三件套(tar.gz + json + meta.json)
354
354
  * 移入 failed/ 同构目录并入队重传;孤立文件与组装目录直接删除。
355
355
  *
356
+ * 两遍扫描:第一遍只处理 .tar.gz(三件套校验 → rescue 或删 tar.gz,并标记
357
+ * 三个文件名已处理)。字母序下 .json/.meta.json 先于 .tar.gz 迭代,若单遍
358
+ * 边迭代边删 .json,会在 tar.gz 校验前破坏三件套——完整崩溃遗留包必然被
359
+ * 误判"不完整"而销毁(曾连续销毁终态包)。第二遍才清理无 tar.gz 对应的
360
+ * 孤儿 .json/meta 与组装残留。
361
+ *
356
362
  * @param stagingDir 暂存目录
357
363
  * @param rescueEnabled 失败保留开关(false 时完整三件套也直接删除,旧行为)
358
364
  * @returns 可补偿重传的包列表(顶层遗留部分)
359
365
  */
360
- function sweepStagingDirectory(stagingDir, rescueEnabled) {
366
+ export function sweepStagingDirectory(stagingDir, rescueEnabled) {
361
367
  const rescued = [];
362
368
  try {
363
369
  const entries = fs.readdirSync(stagingDir);
@@ -365,26 +371,31 @@ function sweepStagingDirectory(stagingDir, rescueEnabled) {
365
371
  const handled = new Set(["failed"]);
366
372
  let removed = 0;
367
373
  const removedNames = [];
374
+ // 第一遍:.tar.gz 三件套判定(必须先于 .json 清理,见函数注释)
368
375
  for (const entry of entries) {
369
376
  if (handled.has(entry))
370
377
  continue;
371
- if (entry.endsWith(".tar.gz")) {
372
- const base = entry.slice(0, -".tar.gz".length);
373
- const pkg = rescueEnabled ? loadIntactPackage(stagingDir, base, names) : undefined;
374
- if (pkg) {
375
- movePackageIntoFailed(stagingDir, pkg, path.join(stagingDir, `${base}.meta.json`));
376
- rescued.push(pkg);
377
- }
378
- else {
379
- fs.rmSync(path.join(stagingDir, entry), { force: true });
380
- removed++;
381
- removedNames.push(entry);
382
- }
383
- handled.add(entry);
384
- handled.add(`${base}.json`);
385
- handled.add(`${base}.meta.json`);
378
+ if (!entry.endsWith(".tar.gz"))
386
379
  continue;
380
+ const base = entry.slice(0, -".tar.gz".length);
381
+ const pkg = rescueEnabled ? loadIntactPackage(stagingDir, base, names) : undefined;
382
+ if (pkg) {
383
+ movePackageIntoFailed(stagingDir, pkg, path.join(stagingDir, `${base}.meta.json`));
384
+ rescued.push(pkg);
385
+ }
386
+ else {
387
+ fs.rmSync(path.join(stagingDir, entry), { force: true });
388
+ removed++;
389
+ removedNames.push(entry);
387
390
  }
391
+ handled.add(entry);
392
+ handled.add(`${base}.json`);
393
+ handled.add(`${base}.meta.json`);
394
+ }
395
+ // 第二遍:清理剩余孤儿(无对应 tar.gz 的快照副本/meta、组装残留目录)
396
+ for (const entry of entries) {
397
+ if (handled.has(entry))
398
+ continue;
388
399
  if (entry.endsWith(".json") || entry.startsWith(".assembly-")) {
389
400
  fs.rmSync(path.join(stagingDir, entry), { recursive: true, force: true });
390
401
  removed++;
@@ -560,6 +571,8 @@ function removeQuietly(filePath) {
560
571
  // ─── 孤儿快照补传(§13 失败补偿链路) ─────────────────────────────────────────
561
572
  /** 已传终态标记目录名(metrics/.sent/<sessionId>) */
562
573
  const SENT_MARKER_DIR = ".sent";
574
+ /** 已传标记时效缓冲:快照 mtime 超出标记时间该值仍视为"标记后更新过"需补传 */
575
+ const SENT_NEWER_GRACE_MS = 60_000;
563
576
  /**
564
577
  * 记录会话终态包已成功上云。
565
578
  * 快照原件(metrics/<sessionId>.json)会永久保留为本地记录,
@@ -578,9 +591,23 @@ export function markSessionUploaded(metricsDir, sessionId) {
578
591
  export function isSessionUploaded(metricsDir, sessionId) {
579
592
  return fs.existsSync(path.join(metricsDir, SENT_MARKER_DIR, sessionId));
580
593
  }
594
+ /** 读取已传标记时间戳(内容为上传完成时刻的毫秒值);无标记/内容非法返回 null */
595
+ function sentTimestamp(metricsDir, sessionId) {
596
+ try {
597
+ const content = fs.readFileSync(path.join(metricsDir, SENT_MARKER_DIR, sessionId), "utf-8").trim();
598
+ const time = Number(content);
599
+ return Number.isFinite(time) && time > 0 ? time : null;
600
+ }
601
+ catch {
602
+ return null;
603
+ }
604
+ }
581
605
  /**
582
606
  * 扫描目录选出孤儿快照候选:mtime 静默 [idleMs, maxAgeMs] 且不在活跃集的快照。
583
- * 上传成功后快照即被删除,"文件仍在"天然等价于"终态未传出"。
607
+ * 快照原件永久保留为本地记录,"已传标记"是防重复上传的依据。
608
+ * 时效判断:标记只证明"标记时刻之前的快照已上云"——会话在中途终态触发
609
+ * (task 完成/session.deleted)上传后仍可能继续运行并更新快照,因此快照
610
+ * mtime 晚于标记时间(超出 60s 缓冲)仍视为孤儿,补传最终态。
584
611
  *
585
612
  * @param metricsDir 快照目录
586
613
  * @param idleMs 静默下限(毫秒)
@@ -602,11 +629,14 @@ export async function selectOrphanSnapshots(metricsDir, idleMs, maxAgeMs, isActi
602
629
  if (!entry.endsWith(".json"))
603
630
  continue;
604
631
  const sessionId = entry.slice(0, -".json".length);
605
- if (isSessionUploaded(metricsDir, sessionId))
606
- continue;
607
632
  const filePath = path.join(metricsDir, entry);
608
633
  try {
609
634
  const stats = await fs.promises.stat(filePath);
635
+ // 已传标记时效判断:标记只证明"标记时刻之前的快照已上云",快照 mtime
636
+ // 晚于标记时间(超出 60s 缓冲)说明会话其后仍更新过,最终态未上云,仍补传
637
+ const sentAt = sentTimestamp(metricsDir, sessionId);
638
+ if (sentAt !== null && stats.mtimeMs <= sentAt + SENT_NEWER_GRACE_MS)
639
+ continue;
610
640
  const age = now - stats.mtimeMs;
611
641
  if (age < idleMs || age > maxAgeMs)
612
642
  continue;
package/dist/plugin.js CHANGED
@@ -23,6 +23,7 @@ export const MetricsPlugin = async (input, options) => {
23
23
  eventsDir: runtime.dirs.eventsDir,
24
24
  enabled: config.enabled !== false,
25
25
  upload: Boolean(runtime.uploadManager),
26
+ agents: runtime.sessionFilter.active ? config.agents : null,
26
27
  configSource: source,
27
28
  });
28
29
  const hooks = {
@@ -31,11 +32,13 @@ export const MetricsPlugin = async (input, options) => {
31
32
  event: event,
32
33
  });
33
34
  },
34
- // 记录各模型 system prompt(只读不改写、不返回;子代理跳过)
35
+ // 记录各模型 system prompt(只读不改写、不返回;子代理跳过;agents 过滤外的会话跳过)
35
36
  "experimental.chat.system.transform": async (input, output) => {
36
37
  const sessionId = input.sessionID;
37
38
  if (!sessionId)
38
39
  return;
40
+ if (!runtime.sessionFilter.shouldTrackSession(sessionId))
41
+ return;
39
42
  if (runtime.eventLogger?.isSubAgent(sessionId))
40
43
  return;
41
44
  const modelId = input.model?.id ?? "unknown";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-metrics-plugin",
3
- "version": "0.3.5",
3
+ "version": "0.4.2",
4
4
  "description": "opencode plugin: auto-records every session's metrics (tokens/rounds/steps/sub-agents/hvigor builds) to snapshot JSON. Zero DB, zero config. Pair with session-viewer for visualization.",
5
5
  "type": "module",
6
6
  "license": "MIT",