@lmzhen/dsh-evolution-curator 0.3.63 → 0.3.65

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/lib/index.js CHANGED
@@ -11,6 +11,13 @@ import { CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEFAULT_ARCHIVE_AFTER_DAYS, DEF
11
11
  */
12
12
  /** Quality-warned skills may turn stale after this many idle days (package-private tunable, P2-8). */
13
13
  const DEFAULT_QUALITY_WARN_STALE_AFTER_DAYS = 7;
14
+ /** B-10 (v18): the optional LLM nomination pass gets its own timeout so a
15
+ * hung/unresponsive provider cannot hold the control-plane mutex forever
16
+ * (`run()` never returns; `restore()`/`consolidate()` queue behind it).
17
+ * 120s matches the review subagent default; the 32-bit ceiling is Node's
18
+ * timer-delay limit (`AbortSignal.timeout` throws above it). */
19
+ const DEFAULT_CURATOR_REVIEW_TIMEOUT_MS = 12e4;
20
+ const MAX_TIMER_DELAY_MS = 2147483647;
14
21
  /**
15
22
  * Block LLM-nominated consolidations that would touch a gate-protected name:
16
23
  * exclude / referenced / suppressed skills must never merge (neither as the
@@ -41,6 +48,7 @@ var EvolutionCurator = class extends Service {
41
48
  autoStart: z.boolean().default(true),
42
49
  bootGraceSeconds: z.number().min(0).default(DEFAULT_CURATOR_BOOT_GRACE_SECONDS),
43
50
  curatorReviewMaxTokens: z.number().min(1).default(DEFAULT_CURATOR_REVIEW_MAX_TOKENS),
51
+ curatorReviewTimeoutMs: z.number().min(1).max(MAX_TIMER_DELAY_MS).default(DEFAULT_CURATOR_REVIEW_TIMEOUT_MS),
44
52
  healthSoftBodyChars: z.number().min(1).default(DEFAULT_HEALTH_THRESHOLDS.softBodyChars),
45
53
  healthStampDensityPerKb: z.number().min(1).default(DEFAULT_HEALTH_THRESHOLDS.stampDensityPerKb),
46
54
  healthChurnMinPatches: z.number().min(1).default(DEFAULT_HEALTH_THRESHOLDS.churnMinPatches)
@@ -62,11 +70,22 @@ var EvolutionCurator = class extends Service {
62
70
  referencedSkillNames;
63
71
  bootGraceSeconds;
64
72
  curatorReviewMaxTokens;
73
+ curatorReviewTimeoutMs;
65
74
  healthSoftBodyChars;
66
75
  healthStampDensityPerKb;
67
76
  healthChurnMinPatches;
68
77
  lastRun = 0;
69
78
  timer;
79
+ /** B-8 (v18): set by the fiber disposer; a triggered autoCheck must not
80
+ * keep mutating the tree after the plugin was disposed. */
81
+ disposed = false;
82
+ /** B-8 (v18): read the disposal flag through a method. The only assignment
83
+ * lives in the disposer closure, which TypeScript's flow analysis cannot
84
+ * see, so a direct `this.disposed` read narrows to the literal `false` and
85
+ * the runtime check would be reported as dead code by `no-unnecessary-condition`. */
86
+ isDisposed() {
87
+ return this.disposed;
88
+ }
70
89
  bootCheck;
71
90
  /** 0.3.18 (E-18): stateless first-run defer fires ONCE per process — the
72
91
  * in-memory clock is seeded and later due runs must proceed, or the
@@ -93,6 +112,10 @@ var EvolutionCurator = class extends Service {
93
112
  this.intervalHours = field("intervalHours", config.intervalHours, DEFAULT_CURATOR_INTERVAL_HOURS, 1);
94
113
  this.staleAfterDays = field("staleAfterDays", config.staleAfterDays, DEFAULT_STALE_AFTER_DAYS, 1);
95
114
  this.archiveAfterDays = field("archiveAfterDays", config.archiveAfterDays, DEFAULT_ARCHIVE_AFTER_DAYS, 1);
115
+ if (this.archiveAfterDays < this.staleAfterDays) {
116
+ this.ctx.logger.warn(`evolution-curator: archiveAfterDays (${this.archiveAfterDays}) < staleAfterDays (${this.staleAfterDays}); using staleAfterDays as the archive threshold`);
117
+ this.archiveAfterDays = this.staleAfterDays;
118
+ }
96
119
  this.llmReview = config.llmReview ?? false;
97
120
  this.curatorProvider = config.curatorProvider ?? "deepseek-official";
98
121
  this.qualityWarnStaleAfterDays = field("qualityWarnStaleAfterDays", config.qualityWarnStaleAfterDays, DEFAULT_QUALITY_WARN_STALE_AFTER_DAYS, 1);
@@ -104,6 +127,7 @@ var EvolutionCurator = class extends Service {
104
127
  this.referencedSkillNames = new Set(config.referencedSkillNames ?? []);
105
128
  this.bootGraceSeconds = field("bootGraceSeconds", config.bootGraceSeconds, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, 0, 3600);
106
129
  this.curatorReviewMaxTokens = field("curatorReviewMaxTokens", config.curatorReviewMaxTokens, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, 1);
130
+ this.curatorReviewTimeoutMs = field("curatorReviewTimeoutMs", config.curatorReviewTimeoutMs, DEFAULT_CURATOR_REVIEW_TIMEOUT_MS, 1, MAX_TIMER_DELAY_MS);
107
131
  this.healthSoftBodyChars = field("healthSoftBodyChars", config.healthSoftBodyChars, DEFAULT_HEALTH_THRESHOLDS.softBodyChars, 1);
108
132
  this.healthStampDensityPerKb = field("healthStampDensityPerKb", config.healthStampDensityPerKb, DEFAULT_HEALTH_THRESHOLDS.stampDensityPerKb, 1);
109
133
  this.healthChurnMinPatches = field("healthChurnMinPatches", config.healthChurnMinPatches, DEFAULT_HEALTH_THRESHOLDS.churnMinPatches, 1);
@@ -111,6 +135,7 @@ var EvolutionCurator = class extends Service {
111
135
  this.lastRun = Date.now();
112
136
  this.ctx.effect(() => {
113
137
  return () => {
138
+ this.disposed = true;
114
139
  this.stop();
115
140
  };
116
141
  }, "evolution-curator.stop");
@@ -125,7 +150,7 @@ var EvolutionCurator = class extends Service {
125
150
  };
126
151
  }
127
152
  start() {
128
- if (!this.enabled || this.timer) return;
153
+ if (this.isDisposed() || !this.enabled || this.timer) return;
129
154
  this.bootCheck = setTimeout(() => {
130
155
  this.bootCheck = void 0;
131
156
  this.autoCheck();
@@ -183,13 +208,16 @@ var EvolutionCurator = class extends Service {
183
208
  * surfaced once instead of silently meaning "never runs".
184
209
  */
185
210
  async autoCheck() {
211
+ if (this.isDisposed()) return;
186
212
  try {
187
213
  const stateService = this.curatorStateService();
188
214
  if (stateService === void 0 && !this.statelessStateWarned) {
189
215
  this.statelessStateWarned = true;
190
216
  this.ctx.logger.warn(`evolution-curator: evolution-state is not mounted — the curation interval baseline is this process's lifetime only (default interval ${DEFAULT_CURATOR_INTERVAL_HOURS}h), so automatic curation will not fire again until the process has been alive that long. Mount evolution-state (evolution-host/all bundle) for a durable schedule.`);
191
217
  }
192
- const last = (await stateService?.loadCuratorState())?.lastRunAt ?? this.lastRun;
218
+ const persisted = await stateService?.loadCuratorState();
219
+ if (this.isDisposed()) return;
220
+ const last = persisted?.lastRunAt ?? this.lastRun;
193
221
  if (Date.now() - last >= this.lifecycle().intervalHours * 36e5) await this.run();
194
222
  } catch (error) {
195
223
  const reason = error instanceof Error ? error.message : String(error);
@@ -258,7 +286,8 @@ var EvolutionCurator = class extends Service {
258
286
  summary: "curator review"
259
287
  }
260
288
  })],
261
- maxTokens: this.curatorReviewMaxTokens
289
+ maxTokens: this.curatorReviewMaxTokens,
290
+ signal: AbortSignal.timeout(this.curatorReviewTimeoutMs)
262
291
  })) assembler.push(chunk);
263
292
  const parsed = parseCuratorNominations(assembler.blocks().filter((block) => block.type === "text").map((block) => block.text).join("\n"));
264
293
  return {
@@ -296,6 +325,14 @@ var EvolutionCurator = class extends Service {
296
325
  * is reversible.
297
326
  */
298
327
  async restoreSnapshot() {
328
+ const release = await this.acquireMutex();
329
+ try {
330
+ return await this.restoreSnapshotCore();
331
+ } finally {
332
+ release();
333
+ }
334
+ }
335
+ async restoreSnapshotCore() {
299
336
  const stateService = this.curatorStateService();
300
337
  const currentState = await stateService?.loadCuratorState();
301
338
  const extras = currentState === null || currentState === void 0 ? [] : [{
@@ -434,6 +471,7 @@ var EvolutionCurator = class extends Service {
434
471
  const root = this.skills.root;
435
472
  const rawUsage = await loadUsage(root, this.io);
436
473
  const usage = dryRun ? new Map([...rawUsage].map(([name, record]) => [name, { ...record }])) : rawUsage;
474
+ const runStartStates = new Map([...usage].map(([name, record]) => [name, record.state]));
437
475
  const snapshotPath = dryRun ? void 0 : await this.snapshotFull("pre-curator-run");
438
476
  const suppressedNames = new Set(await loadSuppressedNames(root, this.io));
439
477
  const gates = new EvolutionGateSet({
@@ -485,6 +523,7 @@ var EvolutionCurator = class extends Service {
485
523
  root,
486
524
  recommendPool: new Set(recommendPool),
487
525
  stateOwned: new Set([...result.transitions.map((t) => t.name), ...archiveCandidates]),
526
+ runStartStates,
488
527
  failedFrom: new Map(result.transitions.filter((t) => t.to === "archived").map((t) => [t.name, t.from]))
489
528
  });
490
529
  if (!dryRun) this.lastRun = Date.now();
@@ -612,6 +651,7 @@ var EvolutionCurator = class extends Service {
612
651
  consolidated: []
613
652
  };
614
653
  const { archiveCandidates, nominations, treeNames, usage, bundledNames, suppressedNames, root, failedFrom } = input;
654
+ const runStartStates = input.runStartStates;
615
655
  const errors = [];
616
656
  const archivedSkills = [];
617
657
  const executedConsolidations = [];
@@ -725,9 +765,13 @@ var EvolutionCurator = class extends Service {
725
765
  this.ctx.logger.warn("evolution-curator: failed to persist suppressed names; archived built-ins may re-enter the lifecycle");
726
766
  }
727
767
  try {
768
+ let skipped = [];
728
769
  await mutateUsage(root, this.io, (disk) => {
729
- foldCuratorFields(disk, usage, stateOwned);
730
- });
770
+ skipped = foldCuratorFields(disk, usage, stateOwned, runStartStates);
771
+ }, { onQuarantine: (message) => {
772
+ this.ctx.logger.warn(`evolution-curator: ${message}`);
773
+ } });
774
+ if (skipped.length > 0) this.ctx.logger.warn(`evolution-curator: lifecycle fold skipped ${skipped.length} name(s) whose on-disk state moved during the run (a concurrent curator/tool won): ${skipped.join(", ")}`);
731
775
  } catch {
732
776
  this.ctx.logger.warn("evolution-curator: failed to persist usage sidecar");
733
777
  }
@@ -891,7 +935,8 @@ var EvolutionCurator = class extends Service {
891
935
  /** marker info (pinned/bundled/hub-installed) per skill, from the library list. */
892
936
  async protectedNameMap() {
893
937
  const map = /* @__PURE__ */ new Map();
894
- for (const summary of await this.skills.list()) if (summary.protectedBy !== null) map.set(summary.name, summary.protectedBy);
938
+ for (const summary of await this.skills.list()) if (summary.protectionUnknown) map.set(summary.name, "unknown");
939
+ else if (summary.protectedBy !== null) map.set(summary.name, summary.protectedBy);
895
940
  return map;
896
941
  }
897
942
  /**
@@ -922,15 +967,21 @@ var EvolutionCurator = class extends Service {
922
967
  await this.snapshotFull("pre-consolidate");
923
968
  const result = await this.skills.consolidate(target, sources);
924
969
  if (!result.ok) return result;
925
- await mutateUsage(this.skills.root, this.io, (disk) => {
926
- for (const source of sources) {
927
- const record = disk.get(source);
928
- if (record) {
929
- record.state = "archived";
930
- record.archived_at = (/* @__PURE__ */ new Date()).toISOString();
970
+ try {
971
+ await mutateUsage(this.skills.root, this.io, (disk) => {
972
+ for (const source of sources) {
973
+ const record = disk.get(source);
974
+ if (record) {
975
+ record.state = "archived";
976
+ record.archived_at = (/* @__PURE__ */ new Date()).toISOString();
977
+ }
931
978
  }
932
- }
933
- });
979
+ }, { onQuarantine: (message) => {
980
+ this.ctx.logger.warn(`evolution-curator: ${message}`);
981
+ } });
982
+ } catch (error) {
983
+ this.ctx.logger.warn(`evolution-curator: failed to persist consolidate usage state: ${error instanceof Error ? error.message : String(error)}`);
984
+ }
934
985
  return result;
935
986
  }
936
987
  /**
@@ -949,13 +1000,19 @@ var EvolutionCurator = class extends Service {
949
1000
  await this.snapshotFull("pre-restore");
950
1001
  const result = await this.skills.restoreFromArchive(name);
951
1002
  if (!result.ok) return result;
952
- await mutateUsage(this.skills.root, this.io, (disk) => {
953
- const record = disk.get(name);
954
- if (record) {
955
- record.state = "active";
956
- record.archived_at = null;
957
- }
958
- });
1003
+ try {
1004
+ await mutateUsage(this.skills.root, this.io, (disk) => {
1005
+ const record = disk.get(name);
1006
+ if (record) {
1007
+ record.state = "active";
1008
+ record.archived_at = null;
1009
+ }
1010
+ }, { onQuarantine: (message) => {
1011
+ this.ctx.logger.warn(`evolution-curator: ${message}`);
1012
+ } });
1013
+ } catch (error) {
1014
+ this.ctx.logger.warn(`evolution-curator: failed to persist restore usage state: ${error instanceof Error ? error.message : String(error)}`);
1015
+ }
959
1016
  if (new Set(await loadSuppressedNames(this.skills.root, this.io)).has(name)) try {
960
1017
  await updateSuppressedNames(this.skills.root, this.io, (current) => {
961
1018
  current.delete(name);
@@ -47,6 +47,8 @@ export interface Config {
47
47
  bootGraceSeconds?: number;
48
48
  /** Max tokens for the optional LLM nomination pass. */
49
49
  curatorReviewMaxTokens?: number;
50
+ /** Timeout (ms) for the optional LLM nomination pass (B-10, v18). */
51
+ curatorReviewTimeoutMs?: number;
50
52
  /** Structure-health soft body limit (chars) — see DEFAULT_HEALTH_THRESHOLDS (rc.73 A1). */
51
53
  healthSoftBodyChars?: number;
52
54
  /** Structure-health stamp-density ceiling per KB — see DEFAULT_HEALTH_THRESHOLDS. */
@@ -104,11 +106,20 @@ export declare class EvolutionCurator extends Service {
104
106
  private readonly referencedSkillNames;
105
107
  private readonly bootGraceSeconds;
106
108
  private readonly curatorReviewMaxTokens;
109
+ private readonly curatorReviewTimeoutMs;
107
110
  private readonly healthSoftBodyChars;
108
111
  private readonly healthStampDensityPerKb;
109
112
  private readonly healthChurnMinPatches;
110
113
  private lastRun;
111
114
  private timer;
115
+ /** B-8 (v18): set by the fiber disposer; a triggered autoCheck must not
116
+ * keep mutating the tree after the plugin was disposed. */
117
+ private disposed;
118
+ /** B-8 (v18): read the disposal flag through a method. The only assignment
119
+ * lives in the disposer closure, which TypeScript's flow analysis cannot
120
+ * see, so a direct `this.disposed` read narrows to the literal `false` and
121
+ * the runtime check would be reported as dead code by `no-unnecessary-condition`. */
122
+ private isDisposed;
112
123
  private bootCheck;
113
124
  /** 0.3.18 (E-18): stateless first-run defer fires ONCE per process — the
114
125
  * in-memory clock is seeded and later due runs must proceed, or the
@@ -178,6 +189,7 @@ export declare class EvolutionCurator extends Service {
178
189
  content: string;
179
190
  }>;
180
191
  }>;
192
+ private restoreSnapshotCore;
181
193
  private skippedReport;
182
194
  /**
183
195
  * Run one curator pass. `ignoreGates` skips the interval and idle gates so an
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-curator",
3
3
  "description": "Deterministic skill lifecycle and recovery (community build)",
4
- "version": "0.3.63",
4
+ "version": "0.3.65",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -31,20 +31,20 @@
31
31
  "license": "MIT",
32
32
  "dependencies": {
33
33
  "@deepseek-ai/schemastery": "^3.18.1",
34
- "@lmzhen/dsh-evolution-core": "^0.3.63"
34
+ "@lmzhen/dsh-evolution-core": "^0.3.65"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "@deepseek-ai/cordis": "^4.0.1",
38
38
  "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
39
39
  "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
40
- "@lmzhen/dsh-evolution-io": "^0.3.63",
41
- "@lmzhen/dsh-evolution-state": "^0.3.63"
40
+ "@lmzhen/dsh-evolution-io": "^0.3.65",
41
+ "@lmzhen/dsh-evolution-state": "^0.3.65"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
45
45
  "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
46
- "@lmzhen/dsh-evolution-core": "^0.3.63",
47
- "@lmzhen/dsh-evolution-io": "^0.3.63",
48
- "@lmzhen/dsh-evolution-state": "^0.3.63"
46
+ "@lmzhen/dsh-evolution-core": "^0.3.65",
47
+ "@lmzhen/dsh-evolution-io": "^0.3.65",
48
+ "@lmzhen/dsh-evolution-state": "^0.3.65"
49
49
  }
50
50
  }