dsh-recency-guard 0.11.0 → 0.11.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.
- package/README.md +42 -0
- package/dist/index.js +124 -33
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -65,6 +65,48 @@
|
|
|
65
65
|
|
|
66
66
|
当某个插件把 DSH 拖垮时,本插件会在崩溃的那一刻记录下"谁最可疑",并在**下一次启动时、在崩溃插件的代码被加载之前**把它挡在门外。反复崩溃时,关闭的插件会逐个增加(第 1 次关最近更新的 1 个,仍崩溃则关最近更新的 2 个)。稳定之后,除首要嫌疑外,其余被关闭的配置会自动恢复。
|
|
67
67
|
|
|
68
|
+
## v0.11.2 紧急修复(恢复诚实性 + 启动恢复时序)
|
|
69
|
+
|
|
70
|
+
- **恢复结果白盒化(P0 假绿修复)**:`reenable` 此前**无条件**返回
|
|
71
|
+
`reenabled: true` 并从 `autoDisabled` 移除 —— 实测管理器返回
|
|
72
|
+
`application: 'failed'`(HMR 嵌套事务)时,state 仍记「已恢复」,面板/工具
|
|
73
|
+
显示成功而插件实际仍关闭。现在只有**真的 applied** 才算成功;失败者保留在
|
|
74
|
+
自动关闭名单待下轮重试,journal/note 如实列出「未启用(管理器拒绝)」。
|
|
75
|
+
- **启动恢复延后到 HMR 事务之外**:boot 阶段 Loader 自身可能正处在事务中,
|
|
76
|
+
恢复调用会报 `HMR transactions cannot be nested`。自动恢复改为 boot 后
|
|
77
|
+
1.5s 执行(定时器 unref,不拖住宿主退出),失败由诚实结果兜底。
|
|
78
|
+
- 回归:host 工具测试新增「管理器拒绝 ⇒ reenable 报失败」断言;14/14 全绿。
|
|
79
|
+
|
|
80
|
+
## v0.11.1 紧急修复(宿主兼容 + 启动崩溃 · 2026-11-08)
|
|
81
|
+
|
|
82
|
+
**事故背景**:桌面端 DSH 自动升级到 `dsh 0.2.2-rc.1` 后连续崩溃(5 次/3 分钟),
|
|
83
|
+
守护插件被兼容闸门移出 profile、不再保护;重新挂载后又因遗留写锁把宿主拖进
|
|
84
|
+
`fatal load failure`。本版本修复三处,全部是「守护不能拖垮宿主」的底线问题。
|
|
85
|
+
|
|
86
|
+
- **宿主预发布版本兼容(P0)**:`engines.dsh` 与全部 `@deepseek-ai/dsh-*`
|
|
87
|
+
peer 范围改为显式预发布分支
|
|
88
|
+
`>=0.2.1-rc.5 <0.2.2-0 || >=0.2.2-rc.1 <0.2.3-0 || >=0.2.3-rc.1 <0.3.0-0`。
|
|
89
|
+
原因:node-semver 只放行「与范围中某个比较符 **major.minor.patch 元组相同**
|
|
90
|
+
且自身带预发布标签」的预发布版本 —— 旧范围 `>=0.2.1-rc.5 <0.3.0-0` 会
|
|
91
|
+
静默排除 `0.2.2-rc.1`,插件被兼容闸门判为不兼容并从 profile 移除。
|
|
92
|
+
已在 `dsh 0.2.2-rc.1` 干净沙箱(sb-rg-v0111)实测安装 + 启动 + 守护激活通过。
|
|
93
|
+
- **启动绝不抛出(P0)**:`boot()` 失败改为**降级留痕**(步骤记失败 + 警告
|
|
94
|
+
日志),不再 rethrow。此前一个可恢复的写锁超时会把宿主变成
|
|
95
|
+
`dsh: fatal load failure`(退出码 1)—— 观测/恢复设施不能拖垮主流程。
|
|
96
|
+
- **失效写锁自愈(P0)**:新增 `cleanStaleLocks`(Store.init 启动时清理 +
|
|
97
|
+
`updateState` 超时后清理重试一次)。判据:锁内容为空/非正整数 PID(不完整
|
|
98
|
+
记录,atomic-write 永远无法接管)/ PID 对应进程已死,且文件年龄 > 5s
|
|
99
|
+
(避开锁创建与写 PID 之间的零字节窗口,绝不抢活锁)。
|
|
100
|
+
真机事故:崩溃恰好发生在锁创建与写 PID 之间,遗留 0 字节
|
|
101
|
+
`state.json.lock`,此后每次启动都等满超时并致命。
|
|
102
|
+
- **连续崩溃兜底升级(P0)**:`assessConfidence` 新增安全网 —— 累计崩溃
|
|
103
|
+
`crashSeq ≥ 3` 时即使概率计数未达 `confirmRounds` 也强制行动;同时
|
|
104
|
+
`readCrashHistory` 跳过启动时的「无嫌疑 crash-detected」记录。实测该记录
|
|
105
|
+
每轮打断连续计数,导致 crashSeq 涨到 3 仍停在「观察中 1/2 轮」,崩溃循环
|
|
106
|
+
永远无法自愈。
|
|
107
|
+
- 测试:新增 `tests/unit/store-lock.test.ts`(锁自愈 11 断言)与契约层
|
|
108
|
+
预发布范围守卫;14/14 全绿(Core 7 / Host 2 / Client 4 / Contract 1)。
|
|
109
|
+
|
|
68
110
|
## v0.11 新增(数据面安全 + 图谱图例一致性 + 马尔可夫去向可见 + 市场准备)
|
|
69
111
|
|
|
70
112
|
- **动作路由方法守卫(P0 安全修复)**:`/action/archive|reenable|restore|recover`
|
package/dist/index.js
CHANGED
|
@@ -62,7 +62,7 @@ function readHostVersion() {
|
|
|
62
62
|
|
|
63
63
|
// src/store.ts
|
|
64
64
|
import { mkdir, readFile, readdir, stat, unlink } from "node:fs/promises";
|
|
65
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
65
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
66
66
|
import { isAbsolute, join, resolve } from "node:path";
|
|
67
67
|
import { writeFileAtomic, withFileLock } from "@deepseek-ai/dsh-atomic-write";
|
|
68
68
|
var STATE_DIRNAME = ".recency-guard";
|
|
@@ -70,6 +70,55 @@ var JOURNAL_MAX_LINES = 1e3;
|
|
|
70
70
|
function emptyState() {
|
|
71
71
|
return { bootMarker: null, crashSeq: 0, autoDisabled: [], stabilized: false, restored: [] };
|
|
72
72
|
}
|
|
73
|
+
var STALE_LOCK_MIN_AGE_MS = 5e3;
|
|
74
|
+
function processAlive(pid) {
|
|
75
|
+
try {
|
|
76
|
+
process.kill(pid, 0);
|
|
77
|
+
return true;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
return error.code === "EPERM";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function cleanStaleLocks(dir, now = Date.now()) {
|
|
83
|
+
const report = { removed: [], kept: [] };
|
|
84
|
+
let entries;
|
|
85
|
+
try {
|
|
86
|
+
entries = readdirSync(dir);
|
|
87
|
+
} catch {
|
|
88
|
+
return report;
|
|
89
|
+
}
|
|
90
|
+
for (const name2 of entries) {
|
|
91
|
+
if (!name2.endsWith(".lock")) continue;
|
|
92
|
+
const path = join(dir, name2);
|
|
93
|
+
try {
|
|
94
|
+
if (now - statSync(path).mtimeMs < STALE_LOCK_MIN_AGE_MS) {
|
|
95
|
+
report.kept.push(name2);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
let text = "";
|
|
99
|
+
try {
|
|
100
|
+
text = readFileSync(path, "utf8").trim();
|
|
101
|
+
} catch {
|
|
102
|
+
report.kept.push(name2);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const pid = Number(text);
|
|
106
|
+
const incomplete = text === "" || !Number.isInteger(pid) || pid <= 0;
|
|
107
|
+
if (!incomplete && processAlive(pid)) {
|
|
108
|
+
report.kept.push(name2);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
rmSync(path, { force: true });
|
|
112
|
+
report.removed.push(name2);
|
|
113
|
+
} catch {
|
|
114
|
+
report.kept.push(name2);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return report;
|
|
118
|
+
}
|
|
119
|
+
function isLockTimeout(error) {
|
|
120
|
+
return error instanceof Error && error.message.includes("timed out waiting for the writer lock");
|
|
121
|
+
}
|
|
73
122
|
var Store = class {
|
|
74
123
|
/** profile 根目录下的守护目录(分区索引与归档都在这里)。 */
|
|
75
124
|
rootDir;
|
|
@@ -105,6 +154,10 @@ var Store = class {
|
|
|
105
154
|
}
|
|
106
155
|
async init() {
|
|
107
156
|
await mkdir(this.backupsDir, { recursive: true });
|
|
157
|
+
try {
|
|
158
|
+
cleanStaleLocks(this.dir);
|
|
159
|
+
} catch {
|
|
160
|
+
}
|
|
108
161
|
}
|
|
109
162
|
/** 本分区的绝对路径(供面板与日志引用)。 */
|
|
110
163
|
get partitionPath() {
|
|
@@ -246,12 +299,23 @@ var Store = class {
|
|
|
246
299
|
}
|
|
247
300
|
/** Serialized read-modify-write across processes. */
|
|
248
301
|
async updateState(mutate) {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
302
|
+
const run = () => withFileLock(
|
|
303
|
+
this.statePath,
|
|
304
|
+
async () => {
|
|
305
|
+
const cur = await this.readState();
|
|
306
|
+
const mutated = await mutate(cur);
|
|
307
|
+
await this.writeState(mutated);
|
|
308
|
+
return mutated;
|
|
309
|
+
},
|
|
310
|
+
{ waitMs: 3e3 }
|
|
311
|
+
);
|
|
312
|
+
try {
|
|
313
|
+
return await run();
|
|
314
|
+
} catch (error) {
|
|
315
|
+
if (!isLockTimeout(error)) throw error;
|
|
316
|
+
cleanStaleLocks(this.dir);
|
|
317
|
+
return run();
|
|
318
|
+
}
|
|
255
319
|
}
|
|
256
320
|
/**
|
|
257
321
|
* Re-arm the crash marker synchronously.
|
|
@@ -2391,16 +2455,19 @@ function assessConfidence(history, currentSuspects, options = {}) {
|
|
|
2391
2455
|
let streak = 1;
|
|
2392
2456
|
for (let i = history.length - 1; i >= 0; i -= 1) {
|
|
2393
2457
|
const entry = history[i];
|
|
2458
|
+
if (entry.suspects.length === 0) continue;
|
|
2394
2459
|
if ((entry.suspects[0] ?? "") !== head) break;
|
|
2395
2460
|
streak += 1;
|
|
2396
2461
|
}
|
|
2397
|
-
const
|
|
2462
|
+
const hardEscalateAfter = Math.max(1, options.hardEscalateAfter ?? 3);
|
|
2463
|
+
const escalated = options.crashSeq !== void 0 && options.crashSeq >= hardEscalateAfter;
|
|
2464
|
+
const act = streak >= confirmRounds || escalated;
|
|
2398
2465
|
return {
|
|
2399
2466
|
act,
|
|
2400
2467
|
evidence: "probabilistic",
|
|
2401
2468
|
rounds: streak,
|
|
2402
2469
|
required: confirmRounds,
|
|
2403
|
-
label: act ? `\u5934\u53F7\u5ACC\u7591 ${head} \u8FDE\u7EED ${streak} \u8F6E\u590D\u73B0\uFF08\u2265 ${confirmRounds}\uFF09\uFF1A\u8FBE\u5230\u884C\u52A8\u9608\u503C` : `\u5934\u53F7\u5ACC\u7591 ${head} \u590D\u73B0 ${streak}/${confirmRounds} \u8F6E\uFF1A\u89C2\u5BDF\u4E2D\uFF0C\u672C\u8F6E\u53EA\u8BB0\u5F55\u4E0D\u5173\u95ED`
|
|
2470
|
+
label: act ? streak >= confirmRounds ? `\u5934\u53F7\u5ACC\u7591 ${head} \u8FDE\u7EED ${streak} \u8F6E\u590D\u73B0\uFF08\u2265 ${confirmRounds}\uFF09\uFF1A\u8FBE\u5230\u884C\u52A8\u9608\u503C` : `\u8FDE\u7EED ${options.crashSeq} \u6B21\u5D29\u6E83\u4ECD\u672A\u6062\u590D\uFF1A\u515C\u5E95\u5347\u7EA7\u4E3A\u884C\u52A8\uFF08\u5B89\u5168\u7F51\uFF09` : `\u5934\u53F7\u5ACC\u7591 ${head} \u590D\u73B0 ${streak}/${confirmRounds} \u8F6E\uFF1A\u89C2\u5BDF\u4E2D\uFF0C\u672C\u8F6E\u53EA\u8BB0\u5F55\u4E0D\u5173\u95ED`
|
|
2404
2471
|
};
|
|
2405
2472
|
}
|
|
2406
2473
|
function environmentChanged(lastEntry, currentRuntime, currentSuspects) {
|
|
@@ -3599,7 +3666,7 @@ async function buildPartitionSnapshot(input, key) {
|
|
|
3599
3666
|
}
|
|
3600
3667
|
|
|
3601
3668
|
// src/archive.ts
|
|
3602
|
-
import { existsSync as existsSync7, mkdirSync as mkdirSync6, readdirSync, readFileSync as readFileSync7, renameSync as renameSync4, statSync as statSync2, writeFileSync as writeFileSync7 } from "node:fs";
|
|
3669
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync6, readdirSync as readdirSync2, readFileSync as readFileSync7, renameSync as renameSync4, statSync as statSync2, writeFileSync as writeFileSync7 } from "node:fs";
|
|
3603
3670
|
import { join as join7 } from "node:path";
|
|
3604
3671
|
function classify(name2) {
|
|
3605
3672
|
if (name2 === "journal.jsonl") return "journal";
|
|
@@ -3632,7 +3699,7 @@ function archiveAndReset(partitionDir2, workspace, reason, now = /* @__PURE__ */
|
|
|
3632
3699
|
}
|
|
3633
3700
|
if (existsSync7(stateDir)) {
|
|
3634
3701
|
mkdirSync6(archiveDir, { recursive: true });
|
|
3635
|
-
for (const name2 of
|
|
3702
|
+
for (const name2 of readdirSync2(stateDir)) {
|
|
3636
3703
|
if (name2 === "archive") continue;
|
|
3637
3704
|
const source = join7(stateDir, name2);
|
|
3638
3705
|
const target = join7(archiveDir, name2);
|
|
@@ -3677,7 +3744,7 @@ function directoryBytes(dir) {
|
|
|
3677
3744
|
let total = 0;
|
|
3678
3745
|
let names;
|
|
3679
3746
|
try {
|
|
3680
|
-
names =
|
|
3747
|
+
names = readdirSync2(dir);
|
|
3681
3748
|
} catch {
|
|
3682
3749
|
return 0;
|
|
3683
3750
|
}
|
|
@@ -3696,7 +3763,7 @@ function listArchives(partitionDir2) {
|
|
|
3696
3763
|
const archiveRoot = join7(partitionDir2, "archive");
|
|
3697
3764
|
if (!existsSync7(archiveRoot)) return [];
|
|
3698
3765
|
const out = [];
|
|
3699
|
-
for (const name2 of
|
|
3766
|
+
for (const name2 of readdirSync2(archiveRoot)) {
|
|
3700
3767
|
const dir = join7(archiveRoot, name2);
|
|
3701
3768
|
try {
|
|
3702
3769
|
const manifest = JSON.parse(readFileSync7(join7(dir, "manifest.json"), "utf8"));
|
|
@@ -4577,6 +4644,7 @@ var Guard = class {
|
|
|
4577
4644
|
for (const e of events) {
|
|
4578
4645
|
if (e.kind !== "crash-detected") continue;
|
|
4579
4646
|
const suspects = Array.isArray(e.suspects) ? e.suspects.filter((s) => typeof s === "string") : [];
|
|
4647
|
+
if (suspects.length === 0) continue;
|
|
4580
4648
|
out.push({
|
|
4581
4649
|
at: e.t,
|
|
4582
4650
|
exitCode: typeof e.exitCode === "number" ? e.exitCode : -1,
|
|
@@ -4752,7 +4820,10 @@ var Guard = class {
|
|
|
4752
4820
|
const targets = pickSuspects(ranked, n);
|
|
4753
4821
|
const history = this.readCrashHistory();
|
|
4754
4822
|
const envChanged = environmentChanged(history[history.length - 1], safeVersion(), ranked);
|
|
4755
|
-
const verdict = assessConfidence(envChanged ? [] : history, ranked, {
|
|
4823
|
+
const verdict = assessConfidence(envChanged ? [] : history, ranked, {
|
|
4824
|
+
confirmRounds: this.config.confirmRounds,
|
|
4825
|
+
crashSeq: seq
|
|
4826
|
+
});
|
|
4756
4827
|
this.store.appendSync({
|
|
4757
4828
|
kind: "note",
|
|
4758
4829
|
detail: `\u6062\u590D\u5224\u5B9A\uFF1A${verdict.label}`
|
|
@@ -4846,7 +4917,10 @@ ${error?.stack ?? ""}
|
|
|
4846
4917
|
this.endStep("boot", true, "\u542F\u52A8\u6D41\u7A0B\u5B8C\u6210");
|
|
4847
4918
|
} catch (error) {
|
|
4848
4919
|
this.endStep("boot", false, describeError(error));
|
|
4849
|
-
|
|
4920
|
+
try {
|
|
4921
|
+
this.ctx.logger?.warn?.(`[recency-guard] \u5B88\u62A4\u542F\u52A8\u964D\u7EA7\uFF08\u5BBF\u4E3B\u7EE7\u7EED\u542F\u52A8\uFF09\uFF1A${describeError(error)}`);
|
|
4922
|
+
} catch {
|
|
4923
|
+
}
|
|
4850
4924
|
}
|
|
4851
4925
|
}
|
|
4852
4926
|
/** Boot 的实现体(外层负责步骤计时与失败原因留痕)。 */
|
|
@@ -4902,11 +4976,15 @@ ${error?.stack ?? ""}
|
|
|
4902
4976
|
`[recency-guard] \u4E0A\u6B21\u5D29\u6E83\u5DF2\u901A\u8FC7\u963B\u6B62\u52A0\u8F7D ${state.autoDisabled.map((d) => d.name).join(", ")} \u6062\u590D\u3002`
|
|
4903
4977
|
);
|
|
4904
4978
|
if (this.config.autoRestore) {
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4979
|
+
const timer = setTimeout(() => {
|
|
4980
|
+
void this.restore().catch((err) => {
|
|
4981
|
+
try {
|
|
4982
|
+
this.ctx.logger.warn(`[recency-guard] \u81EA\u52A8\u6062\u590D\u914D\u7F6E\u5931\u8D25\uFF1A${describeError(err)}`);
|
|
4983
|
+
} catch {
|
|
4984
|
+
}
|
|
4985
|
+
});
|
|
4986
|
+
}, 1500);
|
|
4987
|
+
timer.unref?.();
|
|
4910
4988
|
}
|
|
4911
4989
|
} else if (recovering) {
|
|
4912
4990
|
this.ctx.logger.warn("[recency-guard] \u4E0A\u6B21\u5D29\u6E83\u540E\u542F\u52A8\uFF0C\u4FDD\u6301\u5ACC\u7591\u63D2\u4EF6\u5173\u95ED\u3002");
|
|
@@ -5029,31 +5107,37 @@ ${error?.stack ?? ""}
|
|
|
5029
5107
|
const disabled = new Set(auto.map((d) => d.name));
|
|
5030
5108
|
const reenabled = [];
|
|
5031
5109
|
const keptDisabled = [];
|
|
5110
|
+
const failed = [];
|
|
5032
5111
|
for (const d of auto) {
|
|
5033
5112
|
if (d.name === auto[auto.length - 1]?.name) {
|
|
5034
5113
|
keptDisabled.push(d.name);
|
|
5035
5114
|
continue;
|
|
5036
5115
|
}
|
|
5037
|
-
await this.reenable(d.name);
|
|
5038
|
-
reenabled.push(d.name);
|
|
5116
|
+
const result = await this.reenable(d.name);
|
|
5117
|
+
if (result?.reenabled === true) reenabled.push(d.name);
|
|
5118
|
+
else failed.push(d.name);
|
|
5039
5119
|
}
|
|
5040
5120
|
await this.store.updateState((s) => ({
|
|
5041
5121
|
...s,
|
|
5042
|
-
|
|
5122
|
+
// 失败者留在 autoDisabled(下轮启动重试);成功者与首要嫌疑移出。
|
|
5123
|
+
autoDisabled: s.autoDisabled.filter((d) => keptDisabled.includes(d.name) || failed.includes(d.name)),
|
|
5043
5124
|
stabilized: true,
|
|
5044
5125
|
restored: [...s.restored, ...reenabled]
|
|
5045
5126
|
}));
|
|
5046
5127
|
this.machine.transition("recovered", `\u5DF2\u6062\u590D ${reenabled.length} \u4E2A\u63D2\u4EF6`);
|
|
5047
|
-
await this.store.append({
|
|
5128
|
+
await this.store.append({
|
|
5129
|
+
kind: "restore",
|
|
5130
|
+
detail: `\u91CD\u65B0\u542F\u7528 ${reenabled.join(", ") || "\u65E0"}${failed.length > 0 ? `\uFF1B\u672A\u542F\u7528\uFF08\u7BA1\u7406\u5668\u62D2\u7EDD\uFF0C\u4FDD\u7559\u5F85\u91CD\u8BD5\uFF09 ${failed.join(", ")}` : ""}`
|
|
5131
|
+
});
|
|
5048
5132
|
await this.publishDecision(
|
|
5049
5133
|
[...disabled].map((bundle) => ({ bundle, lastChangeAt: "", lastKind: "install" })),
|
|
5050
|
-
keptDisabled,
|
|
5051
|
-
keptDisabled.length ? "crash-recovery" : "none"
|
|
5134
|
+
[...keptDisabled, ...failed],
|
|
5135
|
+
keptDisabled.length || failed.length ? "crash-recovery" : "none"
|
|
5052
5136
|
);
|
|
5053
5137
|
return {
|
|
5054
5138
|
reenabled,
|
|
5055
|
-
keptDisabled,
|
|
5056
|
-
note: keptDisabled.length ? `\u5DF2\u6062\u590D\
|
|
5139
|
+
keptDisabled: [...keptDisabled, ...failed],
|
|
5140
|
+
note: keptDisabled.length || failed.length ? `\u5DF2\u6062\u590D ${reenabled.length} \u4E2A\u914D\u7F6E\uFF1B${keptDisabled.length > 0 ? `\u4EC5\u4FDD\u7559\u5ACC\u7591\u63D2\u4EF6 ${keptDisabled.join(", ")} \u5173\u95ED` : ""}${failed.length > 0 ? `${keptDisabled.length > 0 ? "\uFF1B" : ""}${failed.join(", ")} \u7BA1\u7406\u5668\u62D2\u7EDD\u6062\u590D\uFF0C\u5DF2\u7559\u5728\u540D\u5355\u5F85\u4E0B\u8F6E\u91CD\u8BD5` : ""}\u3002\u786E\u8BA4\u5B89\u5168\u540E\u53EF\u7528 reenable \u624B\u52A8\u5F00\u542F\u3002` : "\u5DF2\u6062\u590D\u5168\u90E8\u914D\u7F6E\u3002"
|
|
5057
5141
|
};
|
|
5058
5142
|
}
|
|
5059
5143
|
/** Manually re-enable one bundle, noting its backed-up config. */
|
|
@@ -5073,12 +5157,19 @@ ${error?.stack ?? ""}
|
|
|
5073
5157
|
}
|
|
5074
5158
|
}
|
|
5075
5159
|
await this.store.append({ kind: "enable", bundle, result: safeJson(results) });
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5160
|
+
const applied = results.some(
|
|
5161
|
+
(r) => typeof r === "object" && r !== null && r.application === "applied"
|
|
5162
|
+
);
|
|
5163
|
+
if (applied) {
|
|
5164
|
+
await this.store.updateState((s) => ({
|
|
5165
|
+
...s,
|
|
5166
|
+
autoDisabled: s.autoDisabled.filter((d) => d.name !== bundle)
|
|
5167
|
+
}));
|
|
5168
|
+
} else {
|
|
5169
|
+
this.ctx.logger.warn(`[recency-guard] ${bundle} \u672A\u80FD\u6062\u590D\uFF08\u7BA1\u7406\u5668\u672A applied\uFF09\uFF0C\u4FDD\u7559\u5728\u81EA\u52A8\u5173\u95ED\u540D\u5355\u5F85\u4E0B\u8F6E\u91CD\u8BD5`);
|
|
5170
|
+
}
|
|
5080
5171
|
const backup = await this.store.readBackup(bundle);
|
|
5081
|
-
return { bundle, reenabled:
|
|
5172
|
+
return { bundle, reenabled: applied, hasConfigBackup: backup !== null };
|
|
5082
5173
|
}
|
|
5083
5174
|
/**
|
|
5084
5175
|
* Clear the crash marker on teardown.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-recency-guard",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.2",
|
|
4
4
|
"description": "Crash-recovery guard for DeepSeek Harness: records third-party/experimental plugin install & update recency, backs up their config, and after a crash auto-disables the most-recently-updated plugin(s) so the profile boots again, then restores stable config.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -67,10 +67,10 @@
|
|
|
67
67
|
},
|
|
68
68
|
"peerDependencies": {
|
|
69
69
|
"@deepseek-ai/cordis": "~4.0.5",
|
|
70
|
-
"@deepseek-ai/dsh-app-boot": ">=0.2.1-rc.5 <0.3.0-0",
|
|
71
|
-
"@deepseek-ai/dsh-config-editor": ">=0.2.1-rc.5 <0.3.0-0",
|
|
72
|
-
"@deepseek-ai/dsh-plugin-manager": ">=0.2.1-rc.5 <0.3.0-0",
|
|
73
|
-
"@deepseek-ai/dsh-tools": ">=0.2.1-rc.5 <0.3.0-0",
|
|
70
|
+
"@deepseek-ai/dsh-app-boot": ">=0.2.1-rc.5 <0.2.2-0 || >=0.2.2-rc.1 <0.2.3-0 || >=0.2.3-rc.1 <0.3.0-0",
|
|
71
|
+
"@deepseek-ai/dsh-config-editor": ">=0.2.1-rc.5 <0.2.2-0 || >=0.2.2-rc.1 <0.2.3-0 || >=0.2.3-rc.1 <0.3.0-0",
|
|
72
|
+
"@deepseek-ai/dsh-plugin-manager": ">=0.2.1-rc.5 <0.2.2-0 || >=0.2.2-rc.1 <0.2.3-0 || >=0.2.3-rc.1 <0.3.0-0",
|
|
73
|
+
"@deepseek-ai/dsh-tools": ">=0.2.1-rc.5 <0.2.2-0 || >=0.2.2-rc.1 <0.2.3-0 || >=0.2.3-rc.1 <0.3.0-0",
|
|
74
74
|
"@deepseek-ai/schemastery": ">=3.18.0 <4.0.0-0"
|
|
75
75
|
},
|
|
76
76
|
"peerDependenciesMeta": {
|
|
@@ -122,6 +122,6 @@
|
|
|
122
122
|
"typescript": "^5.9.0"
|
|
123
123
|
},
|
|
124
124
|
"engines": {
|
|
125
|
-
"dsh": ">=0.2.1-rc.5 <0.3.0-0"
|
|
125
|
+
"dsh": ">=0.2.1-rc.5 <0.2.2-0 || >=0.2.2-rc.1 <0.2.3-0 || >=0.2.3-rc.1 <0.3.0-0"
|
|
126
126
|
}
|
|
127
127
|
}
|