opencode-metrics-plugin 0.4.3 → 0.4.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.4(2026-09-17)
4
+
5
+ - **移除单包体积上限**
6
+ - **云端日期目录改随会话开始日,单会话一个只存在一个目录**
7
+ - **内置 migration-complete 场景**(label=`migration-complete`,终态):命中 `droid2hmos report validate … --require-complete`(complete 硬门槛唯一正规途径)或 `droid2hmos report render`(仅 complete 后执行)即视为迁移完成,上传成功写 `.sent` 已传标记;容忍 `.cmd`/`.exe` 包装调用。
8
+ - **hap 产物平铺**:包内路径由 `hap/<根名>/<build 剥离相对路径>` 改为 `hap/<原始文件名>`,不再创建深层目录(同名后者覆盖为已知行为)。
9
+ - **黑名单新增 `gradle-home` / `.gradle`**:AI 会话临时发明的 GRADLE_USER_HOME 目录(实测单目录 1.1GB)不再入包。
10
+
3
11
  ## 0.4.3(2026-09-16)
4
12
 
5
13
  - **修复 AGC 上传 400**:Node 原生 http.request 默认不发送 `User-Agent`,AGC 云存储 2026-09-16 起强制校验该头(此前 0.4.2 及更早版本同缺陷,服务端新策略触发)——上传 PUT 显式携带 `User-Agent: opencode-metrics-plugin/<version>`。
package/README.md CHANGED
@@ -57,13 +57,14 @@
57
57
 
58
58
  默认开启(`upload.enabled: false` 可关):
59
59
 
60
- - **触发场景**:bash 执行 `git commit`(label=`git`);会话结束/插件退出(label=`session-end`);**根会话 task 工具完成或失败即刻触发 session-end**(子代理嵌套调用不触发)。可自定义场景(命令正则、事件名或工具名)
61
- - **打包内容**:时点快照 JSON + 事件日志(恒含)+ 会话动过的工作区产物根(鸿蒙工程标记探测,≤3 个)+ build 目录内的 `.hap` 产物包(借道组装目录,不受 build 黑名单影响)
60
+ - **触发场景**:bash 执行 `git commit`(label=`git`);**迁移完成**(`droid2hmos report validate … --require-complete` 或 `report render`,label=`migration-complete`,终态场景);会话结束/插件退出(label=`session-end`);**根会话 task 工具完成或失败即刻触发 session-end**(子代理嵌套调用不触发)。可自定义场景(命令正则、事件名或工具名)
61
+ - **打包内容**:时点快照 JSON + 事件日志(恒含)+ 会话动过的工作区产物根(鸿蒙工程标记探测,≤3 个)+ build 目录内的 `.hap` 产物包(平铺为 `hap/<原始文件名>`,借道组装目录不受 build 黑名单影响)
62
62
  - **根推断增强**:安卓工程根(如 gradle 外壳)不整根保留也不丢弃——向内扫描提升鸿蒙子工程(`ohos/` 等)与迁移工作区(`.migration` / `.harmonyos`);会话期间新建的相邻鸿蒙工程(目录 mtime 在会话开始之后)也会纳入,邻居老工程不混入;纯安卓工程(无任何鸿蒙子根)不打包
63
- - **自动剔除**:黑名单目录(`oh_modules` / `build` / `node_modules` 等 8 项);安卓工程(根级 + 子树级,gradle 信号 ≥2 判定,可用 `excludeAndroidProjects: false` 关闭)
64
- - **云端路径**:`YYYYMMDD/<安卓包名或项目名>-<sessionId>/<agent缩写>-<触发>-v<n>/<agent缩写>-<触发>-v<n>.tar.gz` 与同名 `.json`(一个版本一个目录,双文件归拢;日期取包创建日)
63
+ - **自动剔除**:黑名单目录(`oh_modules` / `build` / `node_modules` / `gradle-home` / `.gradle` 10 项);安卓工程(根级 + 子树级,gradle 信号 ≥2 判定,可用 `excludeAndroidProjects: false` 关闭)
64
+ - **云端路径**:`YYYYMMDD/<安卓包名或项目名>-<sessionId>/<agent缩写>-<触发>-v<n>/<agent缩写>-<触发>-v<n>.tar.gz` 与同名 `.json`(一个版本一个目录,双文件归拢;日期取**会话开始日**,跨天会话归到开始日,旧数据回退包创建日)
65
+ - **无体积上限**:单包不再做大小拦截(原 500MB 上限已移除),超大包交给上传超时(10min)与失败保留机制兜底
65
66
  - **可靠性**:串行队列 + 指数退避重试(默认 5 次,4xx 不重试);失败包移入暂存目录 `failed/` 下**与云端同构的目录**(含 meta 元数据,可整目录手动上传),下次启动自动补偿重传,重传成功即清理;保留超期(默认 7 天)或超量(默认 1GB,从最旧删)自动清理
66
- - **进程死亡兜底**:宿主突然退出(session-end 从未触发)时,下次启动对静默超 24h 且未传终态的快照自动补打 session-end 包(`metrics/.sent/` 已传标记防重传,超 7 天放弃)
67
+ - **进程死亡兜底**:宿主突然退出(session-end 从未触发)时,下次启动对未传终态的快照**立即**自动补打 session-end 包(活跃会话排除;`metrics/.sent/` 已传标记防重传,超 7 天放弃)
67
68
 
68
69
  ## 输出目录
69
70
 
@@ -101,14 +102,13 @@
101
102
  |---|---|---|---|
102
103
  | `upload.enabled` | boolean | `true` | 上传总开关;`false` 显式关闭 |
103
104
  | `upload.uploader.agcConfigPath` | string | 包内凭证 | `agc-apiclient.json` 绝对路径(client_id/secret/project_id/region/bucket) |
104
- | `upload.scenarios` | object | 内置两场景 | 按 key 合并:`enabled:false` 禁用内置;自定义场景提供 `commands`(正则)/ `events`(事件名)/ `tools`(工具名,终态触发)与 `label`(进文件名) |
105
+ | `upload.scenarios` | object | 内置三场景 | 按 key 合并:`enabled:false` 禁用内置;自定义场景提供 `commands`(正则)/ `events`(事件名)/ `tools`(工具名,终态触发)/ `terminal`(终态标记)与 `label`(进文件名) |
105
106
  | `upload.appendArtifacts` | array | — | 追加产物规则:`path`(绝对或相对会话工作目录)+ `matchBy`(`session-activity` 会话动过才纳入 / `existence` 存在即纳入)+ `dirMarkers`/`fileMarkers` |
106
- | `upload.blacklist` | string[] | 默认 8 项 | 打包排除的目录/文件名(追加,不替换) |
107
+ | `upload.blacklist` | string[] | 默认 10 项 | 打包排除的目录/文件名(追加,不替换) |
107
108
  | `upload.excludeAndroidProjects` | boolean | `true` | 剔除安卓工程(根级 + 根内子树) |
108
- | `upload.maxArtifactBytes` | number | `524288000` | 单包体积上限(默认 500MB),超限该次标失败 |
109
109
  | `upload.retry` | object | `{5, 1000}` | `maxAttempts` 最大尝试次数;`backoffMs` 退避基数(1s/4s/16s/64s/256s) |
110
110
  | `upload.retryRescue` | object | `{true, 7, 1GB}` | 失败保留与补偿重传:`enabled` 开关;`retentionDays` 保留天数;`maxFailedBytes` failed/ 总量上限(超限从最旧删) |
111
- | `upload.orphanRescue` | object | `{true, 24, 7}` | 孤儿快照补传:`enabled` 开关;`idleHours` 静默下限(小时);`maxAgeDays` 最大快照年龄(天),超龄放弃 |
111
+ | `upload.orphanRescue` | object | `{true, 0, 7}` | 孤儿快照补传:`enabled` 开关;`idleHours` 静默下限(小时,缺省 0 = 启动即补);`maxAgeDays` 最大快照年龄(天),超龄放弃 |
112
112
 
113
113
  ## 可编程 API
114
114
 
@@ -40,7 +40,7 @@ export declare function loadAgcCredentials(configPath: string, tokenCacheDir?: s
40
40
  export interface AgcUploader {
41
41
  /**
42
42
  * 上传一次触发的两个文件(归档包 + 快照)到
43
- * YYYYMMDD/<projectName>-<sessionId>/<agentName>-<trigger>-v<n>.tar.gz|.json,
43
+ * YYYYMMDD(会话开始日)/<projectName>-<sessionId>/<agentName>-<trigger>-v<n>.tar.gz|.json,
44
44
  * 两者都成功才算成功;任一失败返回该失败(整任务由队列重试)。
45
45
  *
46
46
  * @param uploadPackage 归档产物(含双文件与路径元数据)
@@ -70,7 +70,9 @@ export declare function abbreviateAgentName(agentName: string): string;
70
70
  /**
71
71
  * 组装云端对象路径前缀(含版本目录,双文件归拢其内):
72
72
  * YYYYMMDD/<projectName>-<sessionId>/<agent缩写>-<trigger>-v<n>/<agent缩写>-<trigger>-v<n>
73
- * 日期目录取包创建日(createdAt):重传不漂移到重传当天,失败保留目录与之同构。
73
+ * 日期目录取会话开始日(sessionStartedAt,快照 startTime 跨重启取最早):
74
+ * 跨天会话归到开始日;旧 meta 无此字段回退包创建日(createdAt),重传不漂移到重传当天,
75
+ * 失败保留目录与之同构。
74
76
  */
75
77
  export declare function composeRemoteBase(uploadPackage: UploadPackage): string;
76
78
  /** 归档包云端对象完整路径(上传成功日志用) */
@@ -226,10 +226,12 @@ export function abbreviateAgentName(agentName) {
226
226
  /**
227
227
  * 组装云端对象路径前缀(含版本目录,双文件归拢其内):
228
228
  * YYYYMMDD/<projectName>-<sessionId>/<agent缩写>-<trigger>-v<n>/<agent缩写>-<trigger>-v<n>
229
- * 日期目录取包创建日(createdAt):重传不漂移到重传当天,失败保留目录与之同构。
229
+ * 日期目录取会话开始日(sessionStartedAt,快照 startTime 跨重启取最早):
230
+ * 跨天会话归到开始日;旧 meta 无此字段回退包创建日(createdAt),重传不漂移到重传当天,
231
+ * 失败保留目录与之同构。
230
232
  */
231
233
  export function composeRemoteBase(uploadPackage) {
232
- const created = new Date(uploadPackage.createdAt);
234
+ const created = new Date(uploadPackage.sessionStartedAt ?? uploadPackage.createdAt);
233
235
  const pad = (value) => String(value).padStart(2, "0");
234
236
  const dateDirectory = `${created.getFullYear()}${pad(created.getMonth() + 1)}${pad(created.getDate())}`;
235
237
  const fileBase = `${abbreviateAgentName(uploadPackage.agentName)}-${uploadPackage.trigger}-v${uploadPackage.sequence}`;
@@ -19,8 +19,6 @@ export interface ArchiveSessionOptions {
19
19
  blacklist?: string[];
20
20
  /** 剔除根内嵌套的安卓工程子目录(深度 ≤2),缺省 true */
21
21
  excludeAndroidProjects?: boolean;
22
- /** 单包体积上限,超限该次失败 */
23
- maxArtifactBytes?: number;
24
22
  }
25
23
  /**
26
24
  * 打包一次上传产物:恒含项(快照 + 事件日志)与各 workspace 根打成单个 tar.gz,
@@ -4,9 +4,54 @@ import { spawn } from "node:child_process";
4
4
  import { createHash } from "node:crypto";
5
5
  import { log } from "../../shared/log.js";
6
6
  import { looksLikeAndroidProject } from "./workspaceResolver.js";
7
- import { DEFAULT_BLACKLIST, DEFAULT_MAX_ARTIFACT_BYTES, sanitizeNameSegment, } from "./types.js";
8
- // 会话维度单调序号:v1, v2, ...(进程内;硬杀丢失为 v1 已知限制)
9
- const sessionSequence = new Map();
7
+ import { DEFAULT_BLACKLIST, sanitizeNameSegment, } from "./types.js";
8
+ /**
9
+ * 会话维度单调序号持久化:metrics/<sessionId>.seq 读旧值 +1 写回。
10
+ * 进程内 Map 方案在硬杀/重启后会从 v1 重来,跨进程同日同触发场景会覆盖云端同名对象。
11
+ *
12
+ * @param metricsDir 快照目录
13
+ * @param sessionId 会话 id
14
+ * @returns 本次序号(从 1 起)
15
+ */
16
+ function nextSequence(metricsDir, sessionId) {
17
+ const seqPath = path.join(metricsDir, `${sessionId}.seq`);
18
+ let sequence = 1;
19
+ try {
20
+ const previous = Number(fs.readFileSync(seqPath, "utf-8").trim());
21
+ if (Number.isFinite(previous) && previous > 0)
22
+ sequence = previous + 1;
23
+ }
24
+ catch {
25
+ // 首次打包,从 1 起
26
+ }
27
+ try {
28
+ fs.writeFileSync(seqPath, String(sequence));
29
+ }
30
+ catch {
31
+ // 写失败退化为本次返回值,下次可能重号(与旧的进程内行为一致)
32
+ }
33
+ return sequence;
34
+ }
35
+ /**
36
+ * 从快照提取会话开始时间(ms):顶层 startTime(merge 跨重启取最早)→
37
+ * header.startTime(ISO 字符串)兜底;均无效返回 undefined(云端路径回退 createdAt)。
38
+ *
39
+ * @param snapshotPath 快照文件路径
40
+ */
41
+ function extractSessionStartTime(snapshotPath) {
42
+ try {
43
+ const snapshot = JSON.parse(fs.readFileSync(snapshotPath, "utf-8"));
44
+ if (typeof snapshot.startTime === "number" && snapshot.startTime > 0)
45
+ return snapshot.startTime;
46
+ const parsed = Date.parse(String(snapshot.header?.startTime ?? ""));
47
+ if (Number.isFinite(parsed) && parsed > 0)
48
+ return parsed;
49
+ }
50
+ catch {
51
+ // 快照损坏时回退 createdAt(composeRemoteBase 侧兜底)
52
+ }
53
+ return undefined;
54
+ }
10
55
  let tarAvailable = null;
11
56
  /**
12
57
  * 打包一次上传产物:恒含项(快照 + 事件日志)与各 workspace 根打成单个 tar.gz,
@@ -28,8 +73,7 @@ export async function archiveSessionPackage(options) {
28
73
  }
29
74
  const stagingDir = options.stagingDir;
30
75
  fs.mkdirSync(stagingDir, { recursive: true });
31
- const sequence = (sessionSequence.get(sessionId) ?? 0) + 1;
32
- sessionSequence.set(sessionId, sequence);
76
+ const sequence = nextSequence(metricsDir, sessionId);
33
77
  const agentLabel = sanitizeNameSegment(options.agentName, "agent");
34
78
  const projectLabel = sanitizeNameSegment(options.projectName);
35
79
  const triggerLabel = sanitizeNameSegment(options.trigger, "upload");
@@ -48,16 +92,12 @@ export async function archiveSessionPackage(options) {
48
92
  fs.copyFileSync(logFile, path.join(assemblyDir, "events", path.basename(logFile)));
49
93
  }
50
94
  const rootEntries = assignRootEntryNames(roots);
51
- // hap 产物包独立收集:build 目录整体在 tar 黑名单中,hap 借道组装目录随包上传;
52
- // 目标路径剥去 build 段(否则被 --exclude */build 连坐排除)
95
+ // hap 产物包独立收集:build 目录整体在 tar 黑名单中,hap 借道组装目录随包上传。
96
+ // 平铺落位:直接以原始文件名放 hap/ 下(同名后者覆盖;hap 文件名通常含模块名,实际冲突概率极低)
53
97
  const hapFiles = collectHapFiles(roots, options.blacklist ?? []);
54
98
  let hapBytes = 0;
55
99
  for (const hap of hapFiles) {
56
- const entry = rootEntries.find((candidate) => normalizeRootKey(candidate.root) === normalizeRootKey(hap.root));
57
- if (!entry)
58
- continue;
59
- const targetRelative = hap.relativePath.replace(/(^|\/)build\//, "$1");
60
- const target = path.join(assemblyDir, "hap", entry.entryName, ...targetRelative.split("/"));
100
+ const target = path.join(assemblyDir, "hap", path.basename(hap.filePath));
61
101
  fs.mkdirSync(path.dirname(target), { recursive: true });
62
102
  fs.copyFileSync(hap.filePath, target);
63
103
  hapBytes += fs.statSync(hap.filePath).size;
@@ -69,11 +109,6 @@ export async function archiveSessionPackage(options) {
69
109
  }
70
110
  fs.renameSync(tmpPath, archivePath);
71
111
  const sizeBytes = fs.statSync(archivePath).size;
72
- const maxBytes = options.maxArtifactBytes ?? DEFAULT_MAX_ARTIFACT_BYTES;
73
- if (sizeBytes > maxBytes) {
74
- fs.rmSync(archivePath, { force: true });
75
- return fail("too-large", `包体积 ${sizeBytes} 超过上限 ${maxBytes}`);
76
- }
77
112
  // 快照单独一份(与包内时点一致)
78
113
  fs.copyFileSync(path.join(assemblyDir, "snapshot", `${sessionId}.json`), snapshotPath);
79
114
  const sha256 = await computeSha256(archivePath);
@@ -93,6 +128,7 @@ export async function archiveSessionPackage(options) {
93
128
  sha256,
94
129
  roots: [...roots],
95
130
  createdAt: Date.now(),
131
+ sessionStartedAt: extractSessionStartTime(snapshotSource),
96
132
  };
97
133
  // 元数据 sidecar:失败保留/启动补偿重建上传任务与云端路径的依据,终态时随双文件删除
98
134
  fs.writeFileSync(path.join(stagingDir, `${baseName}.meta.json`), JSON.stringify(uploadPackage, null, 2));
@@ -122,11 +158,6 @@ function assignRootEntryNames(roots) {
122
158
  return { root, entryName };
123
159
  });
124
160
  }
125
- /** 根路径归一化比较键(仅 Windows 大小写不敏感) */
126
- function normalizeRootKey(target) {
127
- const resolved = path.resolve(target);
128
- return process.platform === "win32" ? resolved.toLowerCase() : resolved;
129
- }
130
161
  /** hap 收集递归深度上限(build 目录内部层级防护) */
131
162
  const HAP_SCAN_DEPTH = 10;
132
163
  /**
@@ -11,6 +11,8 @@ export interface ResolvedScenario {
11
11
  events: string[];
12
12
  /** 匹配的工具名(根会话直接调用的工具 completed/error 时触发) */
13
13
  toolNames: string[];
14
+ /** 终态标记:命中上传成功即写 .sent 已传标记 */
15
+ terminal: boolean;
14
16
  }
15
17
  /**
16
18
  * 合并内置场景与 opencode.json 配置:内置 key 深合并(label/commands/events/tools
@@ -9,6 +9,17 @@ const BUILTIN_SCENARIOS = {
9
9
  // git 与 commit 之间允许 ≤4 个选项(如 -c user.name=x、-C <path>、--no-verify),
10
10
  // 但选项必须以 - 开头——避免把 "git log --grep=commit" 一类误判为提交
11
11
  "git-commit": { label: "git", commands: ["\\bgit\\s+(?:-{1,2}[\\w.-]+(?:[ =][^\\s]+)?\\s+){0,4}commit\\b"] },
12
+ // 迁移完成强信号:report validate --require-complete 是 complete 硬门槛的唯一正规途径,
13
+ // report render 仅 complete 后才执行;命中即视为会话业务终态(terminal,上传成功写 .sent 标记)。
14
+ // 前缀容忍 .cmd/.exe 包装调用(Windows 实际形态)
15
+ "migration-complete": {
16
+ label: "migration-complete",
17
+ terminal: true,
18
+ commands: [
19
+ "\\bdroid2hmos(?:\\.cmd|\\.exe)?\\s+report\\s+validate\\b[^\\r\\n]*--require-complete",
20
+ "\\bdroid2hmos(?:\\.cmd|\\.exe)?\\s+report\\s+render\\b",
21
+ ],
22
+ },
12
23
  };
13
24
  /**
14
25
  * 合并内置场景与 opencode.json 配置:内置 key 深合并(label/commands/events/tools
@@ -44,6 +55,7 @@ export function resolveScenarios(configs) {
44
55
  commandRegexes,
45
56
  events: config.events ?? [],
46
57
  toolNames: config.tools ?? [],
58
+ terminal: config.terminal ?? false,
47
59
  });
48
60
  }
49
61
  return result;
@@ -27,6 +27,8 @@ export interface ScenarioConfig {
27
27
  events?: string[];
28
28
  /** 工具名匹配器:根会话直接调用的工具进入终态(completed/error)时触发 */
29
29
  tools?: string[];
30
+ /** 显式终态标记:命中上传成功即写 .sent 已传标记(缺省由 events 含 session.deleted 或 tools 非空推导) */
31
+ terminal?: boolean;
30
32
  }
31
33
  /** 上传通道配置(当前仅 agc) */
32
34
  export interface UploaderConfig {
@@ -52,7 +54,7 @@ export interface RetryRescueConfig {
52
54
  export interface OrphanRescueConfig {
53
55
  /** 缺省 true:启用孤儿快照补传 */
54
56
  enabled?: boolean;
55
- /** 快照静默多久视为孤儿(小时,缺省 24;活跃会话快照始终排除) */
57
+ /** 快照静默多久视为孤儿(小时,缺省 0 = 启动即补,活跃会话快照始终排除) */
56
58
  idleHours?: number;
57
59
  /** 快照最大补传年龄(天,缺省 7,超过视为陈旧放弃) */
58
60
  maxAgeDays?: number;
@@ -73,8 +75,6 @@ export interface UploadConfig {
73
75
  blacklist?: string[];
74
76
  /** 剔除安卓工程(根级剔除 + 根内子树剔除),缺省 true */
75
77
  excludeAndroidProjects?: boolean;
76
- /** 单包体积上限,超限该次标 failed */
77
- maxArtifactBytes?: number;
78
78
  retry?: UploadRetryConfig;
79
79
  retryRescue?: RetryRescueConfig;
80
80
  orphanRescue?: OrphanRescueConfig;
@@ -107,6 +107,8 @@ export interface UploadPackage {
107
107
  /** 纳入的 workspace 产物根 */
108
108
  roots: string[];
109
109
  createdAt: number;
110
+ /** 会话开始时间(ms,取快照 startTime,跨重启取最早):云端日期目录 YYYYMMDD 的来源,随 meta.json 持久化;旧 meta 缺省回退 createdAt */
111
+ sessionStartedAt?: number;
110
112
  }
111
113
  /**
112
114
  * 清洗名称为安全的路径段:非法字符(含空格)替换为 "-",保留中文与内部点号,
@@ -131,8 +133,6 @@ export declare const DEFAULT_DIR_MARKERS: readonly string[];
131
133
  export declare const DEFAULT_FILE_MARKERS: readonly string[];
132
134
  /** 打包排除的目录/文件名(按路径段匹配) */
133
135
  export declare const DEFAULT_BLACKLIST: readonly string[];
134
- /** 单包体积上限默认 500MB */
135
- export declare const DEFAULT_MAX_ARTIFACT_BYTES: number;
136
136
  /** 自证据路径向上探测标记的最大层级 */
137
137
  export declare const MAX_PROBE_DEPTH = 5;
138
138
  /** 产物根数量上限 */
@@ -34,9 +34,9 @@ export const DEFAULT_BLACKLIST = [
34
34
  "node_modules",
35
35
  ".idea",
36
36
  "local.properties",
37
+ "gradle-home",
38
+ ".gradle",
37
39
  ];
38
- /** 单包体积上限默认 500MB */
39
- export const DEFAULT_MAX_ARTIFACT_BYTES = 500 * 1024 * 1024;
40
40
  /** 自证据路径向上探测标记的最大层级 */
41
41
  export const MAX_PROBE_DEPTH = 5;
42
42
  /** 产物根数量上限 */
@@ -31,7 +31,7 @@ export declare function createUploadManager(options: UploadManagerOptions): Uplo
31
31
  * 三个文件名已处理)。字母序下 .json/.meta.json 先于 .tar.gz 迭代,若单遍
32
32
  * 边迭代边删 .json,会在 tar.gz 校验前破坏三件套——完整崩溃遗留包必然被
33
33
  * 误判"不完整"而销毁(曾连续销毁终态包)。第二遍才清理无 tar.gz 对应的
34
- * 孤儿 .json/meta 与组装残留。
34
+ * 孤儿 .json/meta、被中途打断的 .tar.gz.tmp 半成品与组装残留。
35
35
  *
36
36
  * @param stagingDir 暂存目录
37
37
  * @param rescueEnabled 失败保留开关(false 时完整三件套也直接删除,旧行为)
@@ -30,7 +30,7 @@ export function createUploadManager(options) {
30
30
  void uploader.warmup();
31
31
  // 终态场景标签集合:这些标签的包上传成功即写已传标记(孤儿扫描防重依据)
32
32
  const terminalLabels = new Set(scenarios
33
- .filter((s) => s.enabled && (s.events.includes("session.deleted") || s.toolNames.length > 0))
33
+ .filter((s) => s.enabled && (s.terminal || s.events.includes("session.deleted") || s.toolNames.length > 0))
34
34
  .map((s) => s.label));
35
35
  // 失败保留与启动补偿配置(默认开启)
36
36
  const rescueEnabled = config.retryRescue?.enabled !== false;
@@ -93,8 +93,9 @@ export function createUploadManager(options) {
93
93
  queue.enqueue(pkg);
94
94
  }
95
95
  // 孤儿快照补传:宿主死亡导致终态触发从未发生时,按快照年龄补打 session-end 包
96
+ // (idleHours 缺省 0:启动即补,Ctrl+C/崩溃/关窗后无需静默等待;.sent 时效判断防重)
96
97
  if (config.orphanRescue?.enabled !== false) {
97
- const idleMs = (config.orphanRescue?.idleHours ?? 24) * 3_600_000;
98
+ const idleMs = (config.orphanRescue?.idleHours ?? 0) * 3_600_000;
98
99
  const maxAgeMs = (config.orphanRescue?.maxAgeDays ?? 7) * 86_400_000;
99
100
  void rescueOrphanSnapshots(idleMs, maxAgeMs);
100
101
  }
@@ -148,7 +149,6 @@ export function createUploadManager(options) {
148
149
  stagingDir: directories.uploadStagingDir,
149
150
  blacklist: config.blacklist,
150
151
  excludeAndroidProjects: config.excludeAndroidProjects,
151
- maxArtifactBytes: config.maxArtifactBytes,
152
152
  });
153
153
  if (archiveResult.ok) {
154
154
  queue.enqueue(archiveResult.package);
@@ -357,7 +357,7 @@ function dedupePaths(paths) {
357
357
  * 三个文件名已处理)。字母序下 .json/.meta.json 先于 .tar.gz 迭代,若单遍
358
358
  * 边迭代边删 .json,会在 tar.gz 校验前破坏三件套——完整崩溃遗留包必然被
359
359
  * 误判"不完整"而销毁(曾连续销毁终态包)。第二遍才清理无 tar.gz 对应的
360
- * 孤儿 .json/meta 与组装残留。
360
+ * 孤儿 .json/meta、被中途打断的 .tar.gz.tmp 半成品与组装残留。
361
361
  *
362
362
  * @param stagingDir 暂存目录
363
363
  * @param rescueEnabled 失败保留开关(false 时完整三件套也直接删除,旧行为)
@@ -392,11 +392,11 @@ export function sweepStagingDirectory(stagingDir, rescueEnabled) {
392
392
  handled.add(`${base}.json`);
393
393
  handled.add(`${base}.meta.json`);
394
394
  }
395
- // 第二遍:清理剩余孤儿(无对应 tar.gz 的快照副本/meta、组装残留目录)
395
+ // 第二遍:清理剩余孤儿(无对应 tar.gz 的快照副本/meta、被中途打断的 .tar.gz.tmp 半成品、组装残留目录)
396
396
  for (const entry of entries) {
397
397
  if (handled.has(entry))
398
398
  continue;
399
- if (entry.endsWith(".json") || entry.startsWith(".assembly-")) {
399
+ if (entry.endsWith(".json") || entry.endsWith(".tar.gz.tmp") || entry.startsWith(".assembly-")) {
400
400
  fs.rmSync(path.join(stagingDir, entry), { recursive: true, force: true });
401
401
  removed++;
402
402
  removedNames.push(entry);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-metrics-plugin",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
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",