dsh-recency-guard 0.11.1 → 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.
Files changed (3) hide show
  1. package/README.md +12 -0
  2. package/dist/index.js +35 -18
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -65,6 +65,18 @@
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
+
68
80
  ## v0.11.1 紧急修复(宿主兼容 + 启动崩溃 · 2026-11-08)
69
81
 
70
82
  **事故背景**:桌面端 DSH 自动升级到 `dsh 0.2.2-rc.1` 后连续崩溃(5 次/3 分钟),
package/dist/index.js CHANGED
@@ -4976,11 +4976,15 @@ ${error?.stack ?? ""}
4976
4976
  `[recency-guard] \u4E0A\u6B21\u5D29\u6E83\u5DF2\u901A\u8FC7\u963B\u6B62\u52A0\u8F7D ${state.autoDisabled.map((d) => d.name).join(", ")} \u6062\u590D\u3002`
4977
4977
  );
4978
4978
  if (this.config.autoRestore) {
4979
- try {
4980
- await this.restore();
4981
- } catch (err) {
4982
- this.ctx.logger.warn(`[recency-guard] \u81EA\u52A8\u6062\u590D\u914D\u7F6E\u5931\u8D25\uFF1A${describeError(err)}`);
4983
- }
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?.();
4984
4988
  }
4985
4989
  } else if (recovering) {
4986
4990
  this.ctx.logger.warn("[recency-guard] \u4E0A\u6B21\u5D29\u6E83\u540E\u542F\u52A8\uFF0C\u4FDD\u6301\u5ACC\u7591\u63D2\u4EF6\u5173\u95ED\u3002");
@@ -5103,31 +5107,37 @@ ${error?.stack ?? ""}
5103
5107
  const disabled = new Set(auto.map((d) => d.name));
5104
5108
  const reenabled = [];
5105
5109
  const keptDisabled = [];
5110
+ const failed = [];
5106
5111
  for (const d of auto) {
5107
5112
  if (d.name === auto[auto.length - 1]?.name) {
5108
5113
  keptDisabled.push(d.name);
5109
5114
  continue;
5110
5115
  }
5111
- await this.reenable(d.name);
5112
- 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);
5113
5119
  }
5114
5120
  await this.store.updateState((s) => ({
5115
5121
  ...s,
5116
- autoDisabled: s.autoDisabled.filter((d) => keptDisabled.includes(d.name)),
5122
+ // 失败者留在 autoDisabled(下轮启动重试);成功者与首要嫌疑移出。
5123
+ autoDisabled: s.autoDisabled.filter((d) => keptDisabled.includes(d.name) || failed.includes(d.name)),
5117
5124
  stabilized: true,
5118
5125
  restored: [...s.restored, ...reenabled]
5119
5126
  }));
5120
5127
  this.machine.transition("recovered", `\u5DF2\u6062\u590D ${reenabled.length} \u4E2A\u63D2\u4EF6`);
5121
- await this.store.append({ kind: "restore", detail: `\u91CD\u65B0\u542F\u7528 ${reenabled.join(", ") || "\u65E0"}` });
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
+ });
5122
5132
  await this.publishDecision(
5123
5133
  [...disabled].map((bundle) => ({ bundle, lastChangeAt: "", lastKind: "install" })),
5124
- keptDisabled,
5125
- keptDisabled.length ? "crash-recovery" : "none"
5134
+ [...keptDisabled, ...failed],
5135
+ keptDisabled.length || failed.length ? "crash-recovery" : "none"
5126
5136
  );
5127
5137
  return {
5128
5138
  reenabled,
5129
- keptDisabled,
5130
- note: keptDisabled.length ? `\u5DF2\u6062\u590D\u9700\u8981\u7684\u914D\u7F6E\uFF0C\u4EC5\u4FDD\u7559\u5ACC\u7591\u63D2\u4EF6 ${keptDisabled.join(", ")} \u5173\u95ED\u3002\u786E\u8BA4\u5B89\u5168\u540E\u53EF\u7528 reenable \u624B\u52A8\u5F00\u542F\u3002` : "\u5DF2\u6062\u590D\u5168\u90E8\u914D\u7F6E\u3002"
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"
5131
5141
  };
5132
5142
  }
5133
5143
  /** Manually re-enable one bundle, noting its backed-up config. */
@@ -5147,12 +5157,19 @@ ${error?.stack ?? ""}
5147
5157
  }
5148
5158
  }
5149
5159
  await this.store.append({ kind: "enable", bundle, result: safeJson(results) });
5150
- await this.store.updateState((s) => ({
5151
- ...s,
5152
- autoDisabled: s.autoDisabled.filter((d) => d.name !== bundle)
5153
- }));
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
+ }
5154
5171
  const backup = await this.store.readBackup(bundle);
5155
- return { bundle, reenabled: true, hasConfigBackup: backup !== null };
5172
+ return { bundle, reenabled: applied, hasConfigBackup: backup !== null };
5156
5173
  }
5157
5174
  /**
5158
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.1",
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",