dsh-gokifu-compact 1.11.5 → 1.11.7
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 +94 -0
- package/README.md +2 -2
- package/evidence/fold/350/247/246/345/217/221/351/252/214/350/257/201.json +29 -9
- package/package.json +1 -1
- package/src/engine.mjs +29 -6
- package/src/rooms/migrate.mjs +48 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,99 @@
|
|
|
1
1
|
# 更新记录
|
|
2
2
|
|
|
3
|
+
## 1.11.7 — 2026-10-31
|
|
4
|
+
|
|
5
|
+
### 真实测试与门禁都不再「靠输出猜」
|
|
6
|
+
|
|
7
|
+
**问题**:两个门禁的判据都在**猜**,而不是**问退出码**。
|
|
8
|
+
|
|
9
|
+
1. `test/all.mjs` 用正则 `/failed|✗/` 扫子进程输出判失败 —— 测试名里可能**字面含 `✗`**
|
|
10
|
+
(例如 `✓ S10c-3 manifestError 进 ✗ 详情…`)⇒ **把绿当红**;反过来,子进程真失败但输出没命中关键字 ⇒ **漏报**。
|
|
11
|
+
2. `test/real-fold-trigger.mjs` 无论 4/5 还是 5/5 **都 exit 0** ⇒ 它作为门禁时几乎没有意义。
|
|
12
|
+
|
|
13
|
+
**改法**
|
|
14
|
+
|
|
15
|
+
| 文件 | 变更 |
|
|
16
|
+
|---|---|
|
|
17
|
+
| `test/all.mjs` | 删掉 `/failed\|✗/` 正则判据,**只按子进程退出码**;**超时 / 起不来 / 无退出码一律算红**(不许当成「没输出=没事」);导出 `STEPS` / `stepPassed` / `runStep` / `allPassed` 供单测直测 |
|
|
18
|
+
| `test/real-fold-trigger.mjs` | **加退出码**:4/5 ⇒ `exit 1`;依赖未提取(跑不了)⇒ `exit 2` 表示「未运行」而不是「通过」 |
|
|
19
|
+
| 同上 · ⑤ 溢出自救 | 没有可压内容时记 **N/A**(不计失败)—— 先注入新内容让它有东西可压;⑤ 判红不再是「环境没给料」的假失败 |
|
|
20
|
+
| 同上 · usage 对账 | 打印模型返回的 `usage`(**prompt / completion / total / calls**)并累加落盘,真实调用的成本可对账 |
|
|
21
|
+
| 同上 · 预清理 | 跑前**清空 `graphs-trigger`** —— 旧状态会让 ② 的 growth gate 误拦,**第二次跑必红** |
|
|
22
|
+
|
|
23
|
+
**新增 `test/test-gate.test.mjs`(5 条)**:`exit 0` 但输出含 `✗` **不得**误红 / 非 0 **必红** /
|
|
24
|
+
起不来 · 超时 **必红** / 4 通过 1 失败 **必败** / 结构回归(`STEPS` 与导出面)。
|
|
25
|
+
|
|
26
|
+
**变异自检**:`stepPassed` 恒 `true` ⇒ TG-2/TG-3 红;判据退回正则 ⇒ TG-5 红。
|
|
27
|
+
|
|
28
|
+
### 验证
|
|
29
|
+
|
|
30
|
+
- 全量:**486 passed / 0 skipped / 0 failed**(37 个测试文件;481 + 5)
|
|
31
|
+
- `node test/all.mjs` → **exit 0**(5 个步骤全绿:单测 / 挂载校验 / 真实模块 / 真实事务 / 性能基准)
|
|
32
|
+
- `node test/real-fold-trigger.mjs` → **5/5 exit 0**,usage 已打印
|
|
33
|
+
- `verify-install` **16/16** · `bench 2000` **351 tokens**
|
|
34
|
+
- `npm pack --dry-run --json`:**71 文件**(数量不变;`test/` 不进包)
|
|
35
|
+
|
|
36
|
+
**未验证**
|
|
37
|
+
|
|
38
|
+
- `real-fold-trigger` 依赖**真实模型**(走 magpie `127.0.0.1:3425`)与已提取的模块目录;缺依赖时它会 **exit 2(未运行)**,
|
|
39
|
+
这是有意的语义 —— 但也就意味着**它在别的机器上可能根本不跑**,不能当「任何环境都能复现」的证据。
|
|
40
|
+
- `all.mjs` 的「超时必红」用 300s 默认超时,**没有真的等满 300s 去验证**(用的是注入假 `exec` 的单测)。
|
|
41
|
+
|
|
42
|
+
## 1.11.6 — 2026-10-31
|
|
43
|
+
|
|
44
|
+
### tier-2 解耦:checkpoint 的常驻成本不再随 fold 次数线性涨
|
|
45
|
+
|
|
46
|
+
**问题(只读核实)**:fold 的 range 从**上一个 checkpoint 之后**开始 ⇒ 旧 checkpoint **全部留在 surface**
|
|
47
|
+
(实测 `start=1009 > cp8=1008`),每 fold 一次就 +1 条;而旧代码**只在 pressure 分支**才收 tier-2。
|
|
48
|
+
⇒ auto 路径连做 12 次 fold,surface 上就线性涨到 **12 条旧 checkpoint**。
|
|
49
|
+
|
|
50
|
+
**改法**
|
|
51
|
+
|
|
52
|
+
- `src/engine.mjs`:删掉那个**空的 `#tier2`**(调用点 + 定义一起删);常规 fold **成功后**
|
|
53
|
+
(`tier2After > 0`)`await #tier2IfNeeded()`;**失败只 warn,不影响本次 fold**。
|
|
54
|
+
- `#tier2IfNeeded` 加 `_tier2InFlight` **防重入**(同一时刻只允许一个 tier-2 在跑,防递归 / 二次 `compactRegion`)。
|
|
55
|
+
|
|
56
|
+
**前后对照(同一会话 12 次 fold)**
|
|
57
|
+
|
|
58
|
+
| | surface 上旧 checkpoint 条数 | tier-2 次数 |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| 旧 | **12**(`[1..12]`,max 12) | 0(auto 路径从不触发) |
|
|
61
|
+
| 新 | **8**(`[1..8,7,8,7,8]`,max 8) | 14 次 = 12 fold + **2 tier-2** |
|
|
62
|
+
|
|
63
|
+
**测试 6 → 7**:T2-0 改写(空方法已删、fold 后调用、防重入)、T2-5 改写(auto 也触发)、
|
|
64
|
+
T2-6 新增(不递归 / 压回上限 / `tier2After=0` 不触发)。变异:删掉 fold 后的触发 ⇒ T2-0/T2-5/T2-6 红。
|
|
65
|
+
|
|
66
|
+
### 迁移守卫补上 delta:只看 `graph.json` 会漏掉「插件其实在写」
|
|
67
|
+
|
|
68
|
+
**问题**:`migrate-dangling --apply` 的 120s 新鲜度守卫**只 stat `graph.json`**。但 1.9.2 起是**增量落盘** ——
|
|
69
|
+
`graph.json` 只在回合边界写,插件活跃时往往**只有 `graph.json.delta.jsonl` 在动**。
|
|
70
|
+
真实库实测:**8 个会话的 delta 在 120s 内被写过,而旧守卫一个都没拦住** ⇒ apply 可能在插件仍活跃时放行,
|
|
71
|
+
随后内存 flush 把迁移结果覆盖回去(这正是 P6-C 反复强调的 `netCleaned=0` 场景)。
|
|
72
|
+
|
|
73
|
+
**改法**
|
|
74
|
+
|
|
75
|
+
- 新增导出 **`DELTA_NAME = "graph.json.delta.jsonl"`**(与 `kifu-persist.sessionPaths().deltaFile` 的 basename
|
|
76
|
+
必须一致,测试有断言盯着这两个名字)。
|
|
77
|
+
- 新增 **`freshEvidence()`**:返回每个「120s 内被写过」的会话**及具体文件**;`freshSessions()` 在其上去重。
|
|
78
|
+
- 判据:`graph.json` **必看**(缺失照旧抛 ENOENT,不静默跳过);**delta 存在就看 delta**;
|
|
79
|
+
delta 缺失不判 fresh(老库没有 delta 文件也照样能迁移)。
|
|
80
|
+
- 拒绝 apply 时的报错会**点名会话与文件**,而不是只说「有会话新鲜」。
|
|
81
|
+
|
|
82
|
+
**测试 +4**:P6C-11(只有 delta 在写 ⇒ 拦住并点名文件)/ P6C-12(delta 缺失不影响)/ P6C-13(两者都旧 ⇒ 放行)/
|
|
83
|
+
P6C-14(多会话混判:按会话去重,逐个点名)。
|
|
84
|
+
|
|
85
|
+
### 验证
|
|
86
|
+
|
|
87
|
+
- 全量:**481 passed / 0 skipped / 0 failed**(36 个测试文件;476 + 1 tier-2 + 4 migrate)
|
|
88
|
+
- `tier2` **7/0** · `migrate` **14/0** · `engine-harness` 25/0 · `rooms` 15/0 · `verify-install` **16/16**
|
|
89
|
+
- `bench 2000`:**351 tokens**(与基线一致)
|
|
90
|
+
- `npm pack --dry-run --json`:**71 文件**(数量不变:只改 `src/` 与 `test/`,无新增文件)
|
|
91
|
+
|
|
92
|
+
**未验证**
|
|
93
|
+
|
|
94
|
+
- tier-2 的**运行期**收益未在真机观测 —— 8 vs 12 是引擎级前后对照(同一会话、同一 fold 序列)。
|
|
95
|
+
- delta 守卫只在测试夹具与**只读扫描**真实库上验证过;**真实 apply 仍未执行**(需主人授权 + 停插件窗口)。
|
|
96
|
+
|
|
3
97
|
## 1.11.5 — 2026-10-31
|
|
4
98
|
|
|
5
99
|
### S10d · 测试运行器终于能区分「跳过」与「通过」,「空壳用例」不再是绿的
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# dsh-GOkifu-compact
|
|
2
2
|
|
|
3
|
-
> 当前版本:**`1.11.
|
|
3
|
+
> 当前版本:**`1.11.7`** · npm [`dsh-gokifu-compact`](https://www.npmjs.com/package/dsh-gokifu-compact) · [GitHub Releases](https://github.com/MYCF711/dsh-GOkifu-compact/releases) · 更新记录见 [CHANGELOG.md](CHANGELOG.md)
|
|
4
4
|
|
|
5
5
|
**DSH 的棋谱式上下文压缩后端 —— 把「历史 → 一段摘要」换成「历史 → 可复盘的棋谱 DAG + 每步只有 351 tokens 的热清单」。**
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
热清单 351 tokens (2000 边合成夹具 · L5 降级档 · 2026-10-30/10-31 两次实测同值;旧版全量 28,800)
|
|
9
9
|
失败保留 8 / 8 (历史口径 †;官方 8 段模板 4.67 / 8)
|
|
10
10
|
清单开销 未复现/待测 (原载 0.2% = 历史单会话 v1.3.1 后实测;无复跑命令、无对照组;源 evidence/真实计费核对.md)
|
|
11
|
-
验证
|
|
11
|
+
验证 486 项单测(0 skipped) · 真实包 8/8 † · 真实进程 fold 3~6/6 †
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
GO = 围棋,kifu = 棋谱,compact = 压缩。一局棋下完,棋谱上不只有落子顺序,还有每一手的评语。
|
|
@@ -1,31 +1,51 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedAt": "2026-10-
|
|
2
|
+
"generatedAt": "2026-10-31T07:57:46.306Z",
|
|
3
3
|
"model": "group/deepseek-v4.1-flash1017",
|
|
4
4
|
"window": 200000,
|
|
5
5
|
"threshold": 90000,
|
|
6
6
|
"results": [
|
|
7
7
|
{
|
|
8
8
|
"step": "below-threshold",
|
|
9
|
-
"folded": false
|
|
9
|
+
"folded": false,
|
|
10
|
+
"status": "ok"
|
|
10
11
|
},
|
|
11
12
|
{
|
|
12
13
|
"step": "over-threshold",
|
|
13
|
-
"folded":
|
|
14
|
+
"folded": true,
|
|
14
15
|
"before": 25,
|
|
15
|
-
"after":
|
|
16
|
-
"summaryChars":
|
|
16
|
+
"after": 6,
|
|
17
|
+
"summaryChars": 368,
|
|
18
|
+
"status": "ok"
|
|
17
19
|
},
|
|
18
20
|
{
|
|
19
21
|
"step": "growth-gate",
|
|
20
|
-
"folded": false
|
|
22
|
+
"folded": false,
|
|
23
|
+
"status": "ok"
|
|
21
24
|
},
|
|
22
25
|
{
|
|
23
26
|
"step": "second-fold",
|
|
24
|
-
"folded":
|
|
27
|
+
"folded": true,
|
|
28
|
+
"status": "ok"
|
|
25
29
|
},
|
|
26
30
|
{
|
|
27
31
|
"step": "overflow",
|
|
28
|
-
"folded": true
|
|
32
|
+
"folded": true,
|
|
33
|
+
"injected": 6,
|
|
34
|
+
"compressibleTail": 7,
|
|
35
|
+
"status": "ok"
|
|
29
36
|
}
|
|
30
|
-
]
|
|
37
|
+
],
|
|
38
|
+
"summary": {
|
|
39
|
+
"ok": 5,
|
|
40
|
+
"na": 0,
|
|
41
|
+
"fail": 0,
|
|
42
|
+
"total": 5
|
|
43
|
+
},
|
|
44
|
+
"usage": {
|
|
45
|
+
"calls": 3,
|
|
46
|
+
"prompt": 1534,
|
|
47
|
+
"completion": 2738,
|
|
48
|
+
"total": 4272,
|
|
49
|
+
"empty": 0
|
|
50
|
+
}
|
|
31
51
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-gokifu-compact",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.7",
|
|
4
4
|
"description": "DSH context-compression backend with a go game record (kifu) DAG: node = position, edge = attempted move + result. Incremental fold (each span summarized once), failure memory that survives compaction, transposition detection (chess-engine style), and a tiny hot ledger rebuilt every step.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
package/src/engine.mjs
CHANGED
|
@@ -131,6 +131,9 @@ export default class ChessCompactionEngine extends BasicCompactionEngine {
|
|
|
131
131
|
/** ★ P3:每会话基线 graph.json 的内容哈希(16 位);delta 帧用它做 generation 保护。 */
|
|
132
132
|
/** ★ P3:已进入 graph.json/delta 但还没写过 nodes/<id>.md 的节点(fold 时补写)。 */
|
|
133
133
|
this.runtime = new Map();
|
|
134
|
+
/** ★ TIER2-SEMANTICS(task-142):tier-2 重入保护 —— 常规 fold 之后与 pressure 分支都会收 checkpoint,
|
|
135
|
+
* 同一时刻只允许一个 tier-2 在跑(防递归 / 二次 compactRegion)。 */
|
|
136
|
+
this._tier2InFlight = false;
|
|
134
137
|
/** ★ P7:热清单预算统计(跨会话累计;doctor 从这里读)。 */
|
|
135
138
|
this.ledgerBudget = { none: 0, "over-budget": 0, impossible: 0, 未采集: 0, maxOverBudgetBy: 0, updatedAt: 0, samples: 0 };
|
|
136
139
|
if (this.chess.hotLedger) this.#bindLedger();
|
|
@@ -608,7 +611,6 @@ ${constraintsBlock.split("\n").map((l) => `- ${l}`).join("\n")}` });
|
|
|
608
611
|
shadowedTokens: range.shadowedTokens,
|
|
609
612
|
totalAfter: after.totalTokens,
|
|
610
613
|
});
|
|
611
|
-
this.#tier2(session, g);
|
|
612
614
|
// ★ 2026-10-29 修复:这里原来传的是 `{ md: true }`,而 `#flush(session, { tree = false })`
|
|
613
615
|
// 只认 `tree` —— 参数名不匹配 ⇒ fold 后**从不**重写 `nodes/*.md`,
|
|
614
616
|
// 压缩后的节点详情停在第一次落盘的样子(Kimi 评估实测发现)。
|
|
@@ -616,6 +618,20 @@ ${constraintsBlock.split("\n").map((l) => `- ${l}`).join("\n")}` });
|
|
|
616
618
|
this.ctx.logger?.info?.(
|
|
617
619
|
`chess-context: fold #${g.foldCount} 遮蔽 ${range.end - range.start >= 0 ? `${range.start}-${range.end}` : "?"} (~${range.shadowedTokens} tokens) → 总 ${after.totalTokens} tokens(死路 ${failureEdges(g).length} 条,节点 ${g.order.length})`,
|
|
618
620
|
);
|
|
621
|
+
// ★ TIER2-SEMANTICS(task-142,方案 A):checkpoint 常驻成本与「压力」无关,常规 fold 之后也要收。
|
|
622
|
+
// 证据(ws-tier2-semantics/REPORT.md):fold 的 range 从**上一个 checkpoint 之后**开始
|
|
623
|
+
// (实测 8 个 checkpoint 之后 start=1009 > cp8=1008)⇒ 旧 checkpoint 全部留在 surface 上,
|
|
624
|
+
// 每 fold 一次 +1,数量随 fold 次数线性增长。旧版只在 trigger === "pressure" 收,
|
|
625
|
+
// auto / context-overflow 路径会无限堆积(旧的空 `#tier2` 调用不做事)。
|
|
626
|
+
// 安全性:`#tier2IfNeeded` 只调 `compactRegion`(不会回到 `compactIfNeeded`)⇒ 无递归;
|
|
627
|
+
// 另有 `_tier2InFlight` 防重入;`tier2After = 0` 直接跳过(与 compactIfNeeded 的守卫一致)。
|
|
628
|
+
if (this.chess.tier2After > 0) {
|
|
629
|
+
try {
|
|
630
|
+
await this.#tier2IfNeeded(agent, signal);
|
|
631
|
+
} catch (error) {
|
|
632
|
+
this.ctx.logger?.warn?.(`chess-context: fold 后 tier-2 失败(不影响本次 fold):${String(error)}`);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
619
635
|
return result;
|
|
620
636
|
}
|
|
621
637
|
|
|
@@ -640,6 +656,18 @@ ${constraintsBlock.split("\n").map((l) => `- ${l}`).join("\n")}` });
|
|
|
640
656
|
* @returns 合并结果,或 null(不需要/无法合并)
|
|
641
657
|
*/
|
|
642
658
|
async #tier2IfNeeded(agent, signal) {
|
|
659
|
+
// ★ TIER2-SEMANTICS(task-142):防重入 —— 常规 fold 之后与 pressure 分支都会调它。
|
|
660
|
+
// 重入时直接放弃(宁可少合并一次,也不要在一个 fold 事务里套第二个 compactRegion)。
|
|
661
|
+
if (this._tier2InFlight) return null;
|
|
662
|
+
this._tier2InFlight = true;
|
|
663
|
+
try {
|
|
664
|
+
return await this.#tier2IfNeededUnsafe(agent, signal);
|
|
665
|
+
} finally {
|
|
666
|
+
this._tier2InFlight = false;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
async #tier2IfNeededUnsafe(agent, signal) {
|
|
643
671
|
const session = agent.session;
|
|
644
672
|
const cps = checkpointSeqs(session);
|
|
645
673
|
if (cps.length < this.chess.tier2After) return null;
|
|
@@ -665,11 +693,6 @@ ${constraintsBlock.split("\n").map((l) => `- ${l}`).join("\n")}` });
|
|
|
665
693
|
}
|
|
666
694
|
return result;
|
|
667
695
|
}
|
|
668
|
-
|
|
669
|
-
/** 记录一次常规 fold 的元数据(tier-2 由 #tier2IfNeeded 自己记)。 */
|
|
670
|
-
#tier2(_session, _g) {
|
|
671
|
-
/* 保留方法名以免外部调用点断裂;真正的合并在 #tier2IfNeeded。 */
|
|
672
|
-
}
|
|
673
696
|
}
|
|
674
697
|
|
|
675
698
|
/* ────────────────────────────── 辅助 ────────────────────────────── */
|
package/src/rooms/migrate.mjs
CHANGED
|
@@ -239,22 +239,55 @@ export function dryRun(root, { now = Date.now(), only = null } = {}) {
|
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
/**
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
242
|
+
* 增量帧文件名。
|
|
243
|
+
* ⚠ 必须与 `kifu-persist.sessionPaths().deltaFile` 的 basename 一致(那里的值由
|
|
244
|
+
* `<graphFile>.delta.jsonl` 拼出)。测试 `test/migrate.test.mjs` 有断言盯着这两个名字。
|
|
245
|
+
*/
|
|
246
|
+
export const DELTA_NAME = "graph.json.delta.jsonl";
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* 新鲜度**证据**:返回每个「最近 maxAgeMs 内被写过」的会话及**具体文件**。
|
|
250
|
+
*
|
|
251
|
+
* 为什么要看 delta:1.9.2 起是**增量落盘** —— `graph.json` 只在回合边界写,
|
|
252
|
+
* 插件活跃时往往只有 `graph.json.delta.jsonl` 在动。旧实现只 stat `graph.json`,
|
|
253
|
+
* 真实库实测有 8 个会话的 delta 在 120s 内被写却没人拦 ⇒ apply 可能在插件
|
|
254
|
+
* 仍活跃时放行,随后内存 flush 覆盖迁移结果。
|
|
255
|
+
*
|
|
256
|
+
* 判据:
|
|
257
|
+
* · `graph.json` 必看(缺失照旧抛 ENOENT,不静默跳过);
|
|
258
|
+
* · `graph.json.delta.jsonl` 存在才看;
|
|
259
|
+
* · 任一文件的 |now - mtime| < maxAgeMs ⇒ 该会话算 fresh。
|
|
260
|
+
*
|
|
261
|
+
* 用**绝对值**:NTFS 的时间戳可能比 `Date.now()` 超前几毫秒(时钟粒度/回拨),
|
|
262
|
+
* 只看 age>=0 会把「刚写过」误判成「旧文件」,守卫就失效了。
|
|
263
|
+
*
|
|
264
|
+
* @returns {{session: string, file: string, ageMs: number}[]} 命中的(会话, 文件)对
|
|
245
265
|
*/
|
|
246
|
-
export function
|
|
266
|
+
export function freshEvidence(root, { now = Date.now(), maxAgeMs = APPLY_FRESHNESS_MS, only = null } = {}) {
|
|
247
267
|
const out = [];
|
|
248
268
|
for (const dir of selectedSessions(root, only)) {
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
269
|
+
const session = nodePath.basename(dir);
|
|
270
|
+
for (const name of ["graph.json", DELTA_NAME]) {
|
|
271
|
+
const file = nodePath.join(dir, name);
|
|
272
|
+
// graph.json 缺失时 statSync 照旧抛错(与旧行为一致);delta 允许不存在。
|
|
273
|
+
if (name !== "graph.json" && !nodeFs.existsSync(file)) continue;
|
|
274
|
+
const ageMs = Math.abs(now - nodeFs.statSync(file).mtimeMs);
|
|
275
|
+
if (ageMs < maxAgeMs) out.push({ session, file: name, ageMs });
|
|
276
|
+
}
|
|
254
277
|
}
|
|
255
278
|
return out;
|
|
256
279
|
}
|
|
257
280
|
|
|
281
|
+
/**
|
|
282
|
+
* 新鲜度守卫:返回「最近 maxAgeMs 内被写过」的会话(apply 前必须为空)。
|
|
283
|
+
* 为什么需要:运行中的插件会在内存 flush 时把清理结果覆盖回去(实测 netCleaned=0)。
|
|
284
|
+
* 同时看 `graph.json` 与 `graph.json.delta.jsonl`(增量落盘,见 `freshEvidence`)。
|
|
285
|
+
* @returns {string[]} 会话目录名(去重,保留发现顺序)
|
|
286
|
+
*/
|
|
287
|
+
export function freshSessions(root, opts = {}) {
|
|
288
|
+
return [...new Set(freshEvidence(root, opts).map((hit) => hit.session))];
|
|
289
|
+
}
|
|
290
|
+
|
|
258
291
|
/* ───────────────────────────── 备份 ───────────────────────────── */
|
|
259
292
|
|
|
260
293
|
/** 整目录备份到 `<backupRoot>/backup-<ts>/`,返回备份目录与清单。 */
|
|
@@ -310,10 +343,14 @@ export function rollback(backupDir) {
|
|
|
310
343
|
* @throws 有「最近 120s 内被写过」的会话时拒绝执行(插件可能正在跑)
|
|
311
344
|
*/
|
|
312
345
|
export function apply(root, { now = Date.now(), backupRoot = backupsRoot(), stamp = new Date(), maxAgeMs = APPLY_FRESHNESS_MS, only = null } = {}) {
|
|
313
|
-
|
|
346
|
+
// 守卫必须点名**具体文件**:图在 1.9.2 起是增量落盘,只报「graph.json 新鲜」
|
|
347
|
+
// 会让人误以为主图没动就能 apply —— 真正在写的是 delta。
|
|
348
|
+
const fresh = freshEvidence(root, { now, maxAgeMs, only });
|
|
314
349
|
if (fresh.length > 0) {
|
|
350
|
+
const sessions = new Set(fresh.map((hit) => hit.session));
|
|
351
|
+
const detail = fresh.slice(0, 3).map((hit) => `${hit.session} 的 ${hit.file}`).join("、");
|
|
315
352
|
throw new Error(
|
|
316
|
-
`migrate: 拒绝 apply —— ${
|
|
353
|
+
`migrate: 拒绝 apply —— ${sessions.size} 个会话的文件在 ${maxAgeMs / 1000}s 内被写过(${detail}${fresh.length > 3 ? " …" : ""})。` +
|
|
317
354
|
" 运行中的插件会用内存图 flush 覆盖磁盘清理(实测 netCleaned=0);先停插件窗口再重试。",
|
|
318
355
|
);
|
|
319
356
|
}
|