@lmzhen/dsh-evolution-curator 0.16.0 → 0.17.0-rc.1

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/lib/index.js +142 -105
  2. package/lib/types/index.d.ts +131 -39
  3. package/package.json +11 -11
package/lib/index.js CHANGED
@@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto";
3
3
  import { join } from "node:path";
4
4
  import { BlockAssembler, createUserMessage } from "@deepseek-ai/dsh-llm";
5
5
  import z from "@deepseek-ai/schemastery";
6
- import { CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_CURATOR_MODEL, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MIN_IDLE_HOURS, DEFAULT_SKILL_LIMITS, DEFAULT_STALE_AFTER_DAYS, EvolutionGateSet, INSTANCE_KEYS, MAX_TIMER_DELAY_MS, SKILL_NAME_RE, buildCuratorRunReport, claimInstance, clampedNumber, computeDedupGroups, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, emptyRecord, entryTarget, evolutionHome, evolutionIoAdapter, foldCuratorFields, installParamSection, isPresent, isUnknown, loadSuppressedNames, loadUsage, markerEntryName, mutateUsage, newSkillLibrary, paramNamespace, parseCuratorNominations, parseFrontmatter, policyStageLimits, probeList, probeMtime, readNumberParam, relatedSkillNames, releaseInstance, renderCuratorReportMarkdown, transactIo, updateSuppressedNames, usageObserved } from "@lmzhen/dsh-evolution-core";
6
+ import { CURATOR_DRY_RUN_BANNER, CURATOR_PROMPT, DEFAULT_ARCHIVE_AFTER_DAYS, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, DEFAULT_CURATOR_INTERVAL_HOURS, DEFAULT_CURATOR_MODEL, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, DEFAULT_HEALTH_THRESHOLDS, DEFAULT_MIN_IDLE_HOURS, DEFAULT_SKILL_LIMITS, DEFAULT_STALE_AFTER_DAYS, EvolutionGateSet, INSTANCE_KEYS, MAX_TIMER_DELAY_MS, SKILL_NAME_RE, buildCuratorRunReport, claimInstance, clampedNumber, computeDedupGroups, computeLifecycleTransitions, computePrefixClusters, computeQualityScores, computeScopeView, contentHash, emptyRecord, entryTarget, evolutionHome, evolutionIoAdapter, foldCuratorFields, isPresent, isUnknown, loadSuppressedNames, loadUsage, markerEntryName, mutateUsage, newSkillLibrary, paramRowId, parseCuratorNominations, parseFrontmatter, policyStageLimits, probeList, probeMtime, readNumberParam, relatedSkillNames, releaseInstance, renderCuratorReportMarkdown, sessionLastEventTime, transactIo, updateSuppressedNames, usageObserved } from "@lmzhen/dsh-evolution-core";
7
7
  //#region lib/types/index.js
8
8
  /**
9
9
  * Deterministic skill lifecycle curator with interval gate and archive.
@@ -17,23 +17,6 @@ const DEFAULT_QUALITY_WARN_STALE_AFTER_DAYS = 7;
17
17
  * 120s matches the review subagent default; the 32-bit ceiling is Node's
18
18
  * timer-delay limit (`AbortSignal.timeout` throws above it). */
19
19
  const DEFAULT_CURATOR_REVIEW_TIMEOUT_MS = 12e4;
20
- /** Schema the platform validates the user layer against; defaults mirror the core
21
- * constants and the bounds mirror the row schema, so an empty document resolves to
22
- * today's behaviour and an out-of-range value is refused instead of clamped. */
23
- const CURATOR_SETTINGS_SCHEMA = z.object({
24
- curatorIntervalHours: z.number().min(1).default(DEFAULT_CURATOR_INTERVAL_HOURS),
25
- staleAfterDays: z.number().min(1).default(DEFAULT_STALE_AFTER_DAYS),
26
- archiveAfterDays: z.number().min(1).default(DEFAULT_ARCHIVE_AFTER_DAYS),
27
- qualityWarnStaleAfterDays: z.number().min(1).default(DEFAULT_QUALITY_WARN_STALE_AFTER_DAYS),
28
- minIdleHours: z.number().min(0).default(DEFAULT_MIN_IDLE_HOURS),
29
- minIdleFailOpen: z.boolean().default(true),
30
- llmReview: z.boolean().default(false),
31
- curatorReviewMaxTokens: z.number().min(1).default(DEFAULT_CURATOR_REVIEW_MAX_TOKENS),
32
- curatorReviewTimeoutMs: z.number().min(1).max(MAX_TIMER_DELAY_MS).default(DEFAULT_CURATOR_REVIEW_TIMEOUT_MS),
33
- healthSoftBodyChars: z.number().min(1).default(DEFAULT_HEALTH_THRESHOLDS.softBodyChars),
34
- healthStampDensityPerKb: z.number().min(1).default(DEFAULT_HEALTH_THRESHOLDS.stampDensityPerKb),
35
- healthChurnMinPatches: z.number().min(1).default(DEFAULT_HEALTH_THRESHOLDS.churnMinPatches)
36
- });
37
20
  /**
38
21
  * The cross-field rule the schema cannot express (A2-17): an archive threshold
39
22
  * below the stale threshold makes the engine reactivate stale records instead of
@@ -60,54 +43,43 @@ var EvolutionCurator = class extends Service {
60
43
  static Config = z.object({
61
44
  root: z.string().default(""),
62
45
  enabled: z.boolean().default(true),
63
- intervalHours: z.number().min(1).default(DEFAULT_CURATOR_INTERVAL_HOURS),
64
- staleAfterDays: z.number().min(1).default(DEFAULT_STALE_AFTER_DAYS),
65
- archiveAfterDays: z.number().min(1).default(DEFAULT_ARCHIVE_AFTER_DAYS),
66
- llmReview: z.boolean().default(false),
46
+ curatorIntervalHours: z.number().min(1).volatile(),
47
+ intervalHours: z.number().min(1),
48
+ staleAfterDays: z.number().min(1).default(DEFAULT_STALE_AFTER_DAYS).volatile(),
49
+ archiveAfterDays: z.number().min(1).default(DEFAULT_ARCHIVE_AFTER_DAYS).volatile(),
50
+ llmReview: z.boolean().default(false).volatile(),
67
51
  curatorProvider: z.string().default("deepseek-official"),
68
- qualityWarnStaleAfterDays: z.number().min(1).default(DEFAULT_QUALITY_WARN_STALE_AFTER_DAYS),
69
- minIdleHours: z.number().min(0).default(DEFAULT_MIN_IDLE_HOURS),
70
- minIdleFailOpen: z.boolean().default(true),
52
+ qualityWarnStaleAfterDays: z.number().min(1).default(DEFAULT_QUALITY_WARN_STALE_AFTER_DAYS).volatile(),
53
+ minIdleHours: z.number().min(0).default(DEFAULT_MIN_IDLE_HOURS).volatile(),
54
+ minIdleFailOpen: z.boolean().default(true).volatile(),
71
55
  excludeSkillNames: z.array(z.string()).default([]),
72
56
  manageUnmanaged: z.boolean().default(false),
73
57
  pruneBuiltins: z.boolean().default(false),
74
58
  referencedSkillNames: z.array(z.string()).default([]),
75
59
  autoStart: z.boolean().default(true),
76
60
  bootGraceSeconds: z.number().min(0).default(DEFAULT_CURATOR_BOOT_GRACE_SECONDS),
77
- curatorReviewMaxTokens: z.number().min(1).default(DEFAULT_CURATOR_REVIEW_MAX_TOKENS),
78
- curatorReviewTimeoutMs: z.number().min(1).max(MAX_TIMER_DELAY_MS).default(DEFAULT_CURATOR_REVIEW_TIMEOUT_MS),
79
- healthSoftBodyChars: z.number().min(1).default(DEFAULT_HEALTH_THRESHOLDS.softBodyChars),
80
- healthStampDensityPerKb: z.number().min(1).default(DEFAULT_HEALTH_THRESHOLDS.stampDensityPerKb),
81
- healthChurnMinPatches: z.number().min(1).default(DEFAULT_HEALTH_THRESHOLDS.churnMinPatches)
61
+ curatorReviewMaxTokens: z.number().min(1).default(DEFAULT_CURATOR_REVIEW_MAX_TOKENS).volatile(),
62
+ curatorReviewTimeoutMs: z.number().min(1).max(MAX_TIMER_DELAY_MS).default(DEFAULT_CURATOR_REVIEW_TIMEOUT_MS).volatile(),
63
+ healthSoftBodyChars: z.number().min(1).default(DEFAULT_HEALTH_THRESHOLDS.softBodyChars).volatile(),
64
+ healthStampDensityPerKb: z.number().min(1).default(DEFAULT_HEALTH_THRESHOLDS.stampDensityPerKb).volatile(),
65
+ healthChurnMinPatches: z.number().min(1).default(DEFAULT_HEALTH_THRESHOLDS.churnMinPatches).volatile()
82
66
  });
83
67
  skills;
84
68
  io;
85
69
  enabled;
86
- intervalHours;
87
- staleAfterDays;
88
- archiveAfterDays;
89
- llmReview;
70
+ /** This row's live config (G1 §8.3): its volatile fields are resolved at USE time. */
71
+ config;
90
72
  /** S1-C3: one warn per instance when `llmReview` is on but the llm service
91
73
  * is absent — the channel silently degrading must be visible at least once. */
92
74
  llmAbsentWarned = false;
93
75
  curatorProvider;
94
- qualityWarnStaleAfterDays;
95
- minIdleHours;
96
- minIdleFailOpen;
97
76
  excludeSkillNames;
98
77
  manageUnmanaged;
99
78
  pruneBuiltins;
100
79
  referencedSkillNames;
101
80
  bootGraceSeconds;
102
- curatorReviewMaxTokens;
103
- curatorReviewTimeoutMs;
104
- healthSoftBodyChars;
105
- healthStampDensityPerKb;
106
- healthChurnMinPatches;
107
- /** G3/S3.3: this row's resolved values — the base layer of the settings section. */
108
- settingsBase;
109
- /** The user layer for this row's namespace (absent provider = deployment values). */
110
- overrides;
81
+ /** Keys whose supplied value had to be corrected; each warns once (G3.1). */
82
+ clampedKeys = /* @__PURE__ */ new Set();
111
83
  lastRun = 0;
112
84
  timer;
113
85
  /** B-8 (v18): set by the fiber disposer; a triggered autoCheck must not
@@ -138,6 +110,12 @@ var EvolutionCurator = class extends Service {
138
110
  instanceOwner = `evolution-curator[${process.pid}:${randomUUID().slice(0, 8)}]`;
139
111
  constructor(ctx, config = {}) {
140
112
  super(ctx, "evolutionCurator");
113
+ ctx.inject(["settings"], (injected) => {
114
+ const settings = injected.settings;
115
+ if (typeof settings?.configure !== "function") return;
116
+ const disposer = settings.configure({ auto: false });
117
+ if (typeof disposer === "function") ctx.effect(() => disposer, "evolution-curator: settings presentation");
118
+ });
141
119
  this.io = evolutionIoAdapter(() => ctx.evolutionIo.provider());
142
120
  this.skills = newSkillLibrary({
143
121
  config,
@@ -148,62 +126,31 @@ var EvolutionCurator = class extends Service {
148
126
  ...policyStageLimits(this.ctx.get("evolutionPolicy")?.get?.())
149
127
  }
150
128
  });
129
+ this.config = config;
151
130
  this.enabled = config.enabled ?? true;
152
131
  this.instanceHome = evolutionHome();
153
132
  const claim = claimInstance(this.instanceHome, INSTANCE_KEYS.curator, this.instanceOwner);
154
133
  this.holdsInstance = claim.granted;
155
134
  if (!claim.granted) this.ctx.logger.warn(`evolution-curator: this instance YIELDS — ${claim.key} is already held by ${claim.holder} IN THIS PROCESS (the claim is a module-scope Map, so it does not exclude another process); a second curator ROW over one home would run a second gate set and a second lifecycle pass over the same tree. It schedules nothing and every run() returns skipped "instance-held"; the report sweep itself is cross-process locked (v43 FLOW2-1).`);
156
- const clamped = [];
157
- const field = (name, value, fallback, min, max) => {
158
- const result = clampedNumber(value, fallback, max === void 0 ? { min } : {
159
- min,
160
- max
161
- });
162
- if (value !== void 0 && result !== value) clamped.push(name);
163
- return result;
164
- };
165
- this.intervalHours = field("intervalHours", readNumberParam(config, "curatorIntervalHours"), DEFAULT_CURATOR_INTERVAL_HOURS, 1);
166
- this.staleAfterDays = field("staleAfterDays", config.staleAfterDays, DEFAULT_STALE_AFTER_DAYS, 1);
167
- this.archiveAfterDays = field("archiveAfterDays", config.archiveAfterDays, DEFAULT_ARCHIVE_AFTER_DAYS, 1);
168
- if (this.archiveAfterDays < this.staleAfterDays) {
169
- this.ctx.logger.warn(`evolution-curator: archiveAfterDays (${this.archiveAfterDays}) < staleAfterDays (${this.staleAfterDays}); using staleAfterDays as the archive threshold`);
170
- this.archiveAfterDays = this.staleAfterDays;
171
- }
172
- this.llmReview = config.llmReview ?? false;
173
135
  this.curatorProvider = config.curatorProvider ?? "deepseek-official";
174
- this.qualityWarnStaleAfterDays = field("qualityWarnStaleAfterDays", config.qualityWarnStaleAfterDays, DEFAULT_QUALITY_WARN_STALE_AFTER_DAYS, 1);
175
- this.minIdleHours = field("minIdleHours", config.minIdleHours, DEFAULT_MIN_IDLE_HOURS, 0);
176
- this.minIdleFailOpen = config.minIdleFailOpen ?? true;
177
136
  this.excludeSkillNames = new Set(config.excludeSkillNames ?? []);
178
137
  this.manageUnmanaged = config.manageUnmanaged ?? false;
179
138
  this.pruneBuiltins = config.pruneBuiltins ?? false;
180
139
  this.referencedSkillNames = new Set(config.referencedSkillNames ?? []);
181
- this.bootGraceSeconds = field("bootGraceSeconds", config.bootGraceSeconds, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, 0, 3600);
182
- this.curatorReviewMaxTokens = field("curatorReviewMaxTokens", config.curatorReviewMaxTokens, DEFAULT_CURATOR_REVIEW_MAX_TOKENS, 1);
183
- this.curatorReviewTimeoutMs = field("curatorReviewTimeoutMs", config.curatorReviewTimeoutMs, DEFAULT_CURATOR_REVIEW_TIMEOUT_MS, 1, MAX_TIMER_DELAY_MS);
184
- this.healthSoftBodyChars = field("healthSoftBodyChars", config.healthSoftBodyChars, DEFAULT_HEALTH_THRESHOLDS.softBodyChars, 1);
185
- this.healthStampDensityPerKb = field("healthStampDensityPerKb", config.healthStampDensityPerKb, DEFAULT_HEALTH_THRESHOLDS.stampDensityPerKb, 1);
186
- this.healthChurnMinPatches = field("healthChurnMinPatches", config.healthChurnMinPatches, DEFAULT_HEALTH_THRESHOLDS.churnMinPatches, 1);
187
- if (clamped.length > 0) this.ctx.logger.warn(`evolution-curator: ${clamped.join(", ")} provided an invalid value; falling back to the default`);
188
- this.settingsBase = {
189
- curatorIntervalHours: this.intervalHours,
190
- staleAfterDays: this.staleAfterDays,
191
- archiveAfterDays: this.archiveAfterDays,
192
- qualityWarnStaleAfterDays: this.qualityWarnStaleAfterDays,
193
- minIdleHours: this.minIdleHours,
194
- minIdleFailOpen: this.minIdleFailOpen,
195
- llmReview: this.llmReview,
196
- curatorReviewMaxTokens: this.curatorReviewMaxTokens,
197
- curatorReviewTimeoutMs: this.curatorReviewTimeoutMs,
198
- healthSoftBodyChars: this.healthSoftBodyChars,
199
- healthStampDensityPerKb: this.healthStampDensityPerKb,
200
- healthChurnMinPatches: this.healthChurnMinPatches
201
- };
202
- this.overrides = installParamSection(ctx, paramNamespace("evolution-curator"), CURATOR_SETTINGS_SCHEMA, this.settingsBase, {
203
- warn: (message) => {
204
- this.ctx.logger.warn("dsh-evolution-curator: " + message);
205
- },
206
- validate: validateCuratorSettings
140
+ this.bootGraceSeconds = this.clampOnce("bootGraceSeconds", config.bootGraceSeconds, DEFAULT_CURATOR_BOOT_GRACE_SECONDS, 0, 3600);
141
+ const archiveAtBoot = this.rowNumber("archiveAfterDays", DEFAULT_ARCHIVE_AFTER_DAYS, 1);
142
+ const staleAtBoot = this.rowNumber("staleAfterDays", DEFAULT_STALE_AFTER_DAYS, 1);
143
+ if (archiveAtBoot < staleAtBoot) this.ctx.logger.warn(`evolution-curator: archiveAfterDays (${archiveAtBoot}) < staleAfterDays (${staleAtBoot}); using staleAfterDays as the archive threshold`);
144
+ ctx.on("internal/config", function(_previous, next) {
145
+ const value = next();
146
+ if (this !== ctx.fiber) return value;
147
+ const archive = readNumberParam(value, "archiveAfterDays");
148
+ const stale = readNumberParam(value, "staleAfterDays");
149
+ if (typeof archive === "number" && typeof stale === "number") validateCuratorSettings({
150
+ archiveAfterDays: archive,
151
+ staleAfterDays: stale
152
+ });
153
+ return value;
207
154
  });
208
155
  this.lastRun = Date.now();
209
156
  this.ctx.effect(() => {
@@ -215,24 +162,112 @@ var EvolutionCurator = class extends Service {
215
162
  }, "evolution-curator.stop");
216
163
  if (config.autoStart ?? true) this.start();
217
164
  }
165
+ /**
166
+ * Clamp one supplied numeric value, warning once per key (G3.1).
167
+ * @param key - the config key, for the warning.
168
+ * @param value - the value as supplied by the deployment or the user.
169
+ * @param fallback - the package default a corrected value falls back to.
170
+ * @param min - the smallest value the engine can act on.
171
+ * @param max - the largest value the carrier can hold, when bounded.
172
+ * @returns the value the engine may use.
173
+ */
174
+ clampOnce(key, value, fallback, min, max) {
175
+ const result = clampedNumber(value, fallback, max === void 0 ? { min } : {
176
+ min,
177
+ max
178
+ });
179
+ if (value !== void 0 && result !== value && !this.clampedKeys.has(key)) {
180
+ this.clampedKeys.add(key);
181
+ this.ctx.logger.warn(`evolution-curator: ${key} provided an invalid value; falling back to the default`);
182
+ }
183
+ return result;
184
+ }
185
+ /**
186
+ * One live numeric row field at USE time (G1 §8.3): the row config carries the
187
+ * platform-resolved layers (schema default < bundle row < profile override) and the
188
+ * field stays a live reference, so a committed edit applies without a watcher.
189
+ * @param key - the canonical registry id of the field.
190
+ * @returns the current plain value, or undefined when nothing supplied one.
191
+ */
192
+ liveNumber(key) {
193
+ if (key === "curatorIntervalHours") return this.config.curatorIntervalHours?.get() ?? this.config.intervalHours;
194
+ return this.config[key]?.get();
195
+ }
196
+ /**
197
+ * One live numeric row field, clamped and warned once (G3.1).
198
+ * @param key - the canonical registry id of the field.
199
+ * @param fallback - the package default a corrected value falls back to.
200
+ * @param min - the smallest value the engine can act on.
201
+ * @param max - the largest value the carrier can hold, when bounded.
202
+ * @returns the value the engine may use.
203
+ */
204
+ rowNumber(key, fallback, min, max) {
205
+ return this.clampOnce(key, this.liveNumber(key), fallback, min, max);
206
+ }
207
+ /**
208
+ * One live boolean row field at USE time.
209
+ * @param key - the canonical registry id of the field.
210
+ * @param fallback - the value used when the deployment supplied none.
211
+ * @returns the current value.
212
+ */
213
+ rowBoolean(key, fallback) {
214
+ return this.config[key]?.get() ?? fallback;
215
+ }
216
+ /**
217
+ * This row's resolved values — the base layer of the precedence chain (G1 §8.3).
218
+ * Resolved per call so a user edit is live; every numeric field is clamped.
219
+ * @returns the row layer of the curator's behaviour knobs.
220
+ */
221
+ rowSettings() {
222
+ const number = (key, fallback, min, max) => this.rowNumber(key, fallback, min, max);
223
+ return {
224
+ curatorIntervalHours: number("curatorIntervalHours", DEFAULT_CURATOR_INTERVAL_HOURS, 1),
225
+ staleAfterDays: number("staleAfterDays", DEFAULT_STALE_AFTER_DAYS, 1),
226
+ archiveAfterDays: number("archiveAfterDays", DEFAULT_ARCHIVE_AFTER_DAYS, 1),
227
+ qualityWarnStaleAfterDays: number("qualityWarnStaleAfterDays", DEFAULT_QUALITY_WARN_STALE_AFTER_DAYS, 1),
228
+ minIdleHours: number("minIdleHours", DEFAULT_MIN_IDLE_HOURS, 0),
229
+ minIdleFailOpen: this.rowBoolean("minIdleFailOpen", true),
230
+ llmReview: this.rowBoolean("llmReview", false),
231
+ curatorReviewMaxTokens: number("curatorReviewMaxTokens", DEFAULT_CURATOR_REVIEW_MAX_TOKENS, 1),
232
+ curatorReviewTimeoutMs: number("curatorReviewTimeoutMs", DEFAULT_CURATOR_REVIEW_TIMEOUT_MS, 1, MAX_TIMER_DELAY_MS),
233
+ healthSoftBodyChars: number("healthSoftBodyChars", DEFAULT_HEALTH_THRESHOLDS.softBodyChars, 1),
234
+ healthStampDensityPerKb: number("healthStampDensityPerKb", DEFAULT_HEALTH_THRESHOLDS.stampDensityPerKb, 1),
235
+ healthChurnMinPatches: number("healthChurnMinPatches", DEFAULT_HEALTH_THRESHOLDS.churnMinPatches, 1)
236
+ };
237
+ }
238
+ /**
239
+ * The keys the user set in this row's namespace (G1 §8.4-A).
240
+ *
241
+ * This is the ONLY remaining read of the settings user layer, and it reads KEY
242
+ * NAMES, never values: the platform resolves the user layer into the row config,
243
+ * so a value is needed from `config` — but the precedence rule (user > policy)
244
+ * needs to know WHICH keys the user set, or a deployment policy would silently
245
+ * override the user.
246
+ * @returns the keys present in this row's user layer.
247
+ */
248
+ userSetKeys() {
249
+ const entry = this.ctx.get("settings")?.describe?.({ redactSecrets: false }).find((item) => item.ns === paramRowId("evolution-curator"));
250
+ return new Set(Object.keys(entry?.user ?? {}));
251
+ }
218
252
  /** The policy-snapshot carrier for this group (unmounted service = no policy). */
219
253
  policyFields() {
220
254
  return this.ctx.get("evolutionPolicy")?.get() ?? {};
221
255
  }
222
256
  /**
223
- * G3/S3.3: the ONE reader of the curator's behaviour knobs. Precedence is
224
- * user > policy snapshot > this row, and {@link ParamOverrides.get} returns
225
- * undefined for a key the user never set, so the deployment carriers keep
226
- * winning exactly as before. Read at USE time: a committed settings change is
227
- * live without a restart and without a watcher.
257
+ * G3/S3.3 + G1: the ONE reader of the curator's behaviour knobs. Precedence is
258
+ * user > policy snapshot > this row: the row layer already carries the user value
259
+ * (a live field), and {@link userSetKeys} answers whether the user set the key —
260
+ * so an untouched key keeps following the deployment carriers exactly as before.
261
+ * Read at USE time: a committed settings change is live without a restart and
262
+ * without a watcher.
228
263
  * @returns the resolved section, every numeric field sanitized.
229
264
  */
230
265
  settings() {
231
266
  const snapshot = this.policyFields();
232
- const row = this.settingsBase;
267
+ const row = this.rowSettings();
268
+ const userSet = this.userSetKeys();
233
269
  const pick = (key) => {
234
- const user = this.overrides.get(key);
235
- if (user !== void 0) return user;
270
+ if (userSet.has(key)) return row[key];
236
271
  return snapshot[key] ?? row[key];
237
272
  };
238
273
  const number = (key, min, max) => {
@@ -412,8 +447,7 @@ var EvolutionCurator = class extends Service {
412
447
  text: prompt
413
448
  }],
414
449
  source: {
415
- kind: "plugin",
416
- plugin: "dsh-evolution-curator",
450
+ kind: "evolution-curator",
417
451
  form: "notice",
418
452
  summary: "curator review"
419
453
  }
@@ -1049,11 +1083,14 @@ var EvolutionCurator = class extends Service {
1049
1083
  const agents = this.ctx.get("agents");
1050
1084
  if (!agents) return !settings.minIdleFailOpen;
1051
1085
  let latest = 0;
1086
+ let measured = 0;
1052
1087
  for (const agent of agents.list()) {
1053
- const events = agent.session.snapshotEvents();
1054
- const last = events.length === 0 ? 0 : events[events.length - 1]?.time ?? 0;
1088
+ const last = sessionLastEventTime(this.ctx, agent.session);
1089
+ if (last === void 0) continue;
1090
+ measured += 1;
1055
1091
  latest = Math.max(latest, last);
1056
1092
  }
1093
+ if (measured === 0) return !settings.minIdleFailOpen;
1057
1094
  return latest > 0 && Date.now() - latest < settings.minIdleHours * 36e5;
1058
1095
  }
1059
1096
  /**
@@ -1435,4 +1472,4 @@ function reportsSweepLockTarget() {
1435
1472
  return join(evolutionHome(), "reports", ".retention");
1436
1473
  }
1437
1474
  //#endregion
1438
- export { CURATOR_SETTINGS_SCHEMA, EvolutionCurator, EvolutionCurator as default, gateConsolidations, validateCuratorSettings };
1475
+ export { EvolutionCurator, EvolutionCurator as default, gateConsolidations, validateCuratorSettings };
@@ -3,8 +3,16 @@
3
3
  * @module @lmzhen/dsh-evolution-curator
4
4
  */
5
5
  import { Context, Service } from '@deepseek-ai/cordis';
6
+ import type { Volatile } from '@deepseek-ai/cordis';
7
+ import type { ContextFormed } from '@deepseek-ai/dsh-llm';
8
+ declare module '@deepseek-ai/dsh-llm' {
9
+ interface MessageSourceMap {
10
+ 'evolution-curator': {
11
+ kind: 'evolution-curator';
12
+ } & ContextFormed;
13
+ }
14
+ }
6
15
  import z from '@deepseek-ai/schemastery';
7
- import type Schema from '@deepseek-ai/schemastery';
8
16
  import { EvolutionGateSet, type SkillLibrary } from '@lmzhen/dsh-evolution-core';
9
17
  import { type CuratorConsolidation, type CuratorNominations, type CuratorRunReport, type ScopeView, type SkillActionResult, type SkillHealthVerdict } from '@lmzhen/dsh-evolution-core';
10
18
  import type { SkillVersion } from '@lmzhen/dsh-evolution-core';
@@ -16,29 +24,31 @@ declare module '@deepseek-ai/cordis' {
16
24
  }
17
25
  export interface Config {
18
26
  enabled?: boolean;
27
+ /** Canonical registry id for the due-ness interval (see PARAM_ALIASES). */
28
+ curatorIntervalHours?: Volatile<number | undefined>;
19
29
  /** The due-ness interval. The policy snapshot shadows it in every shipped
20
30
  * composition — configure `curatorIntervalHours` there; that name is the
21
31
  * canonical id. Deprecated alias (G0/S0.3): still readable, refused by
22
32
  * writes; removed 0.7.0. */
23
33
  intervalHours?: number;
24
- staleAfterDays?: number;
25
- archiveAfterDays?: number;
34
+ staleAfterDays?: Volatile<number>;
35
+ archiveAfterDays?: Volatile<number>;
26
36
  /** Skill-tree root for curator scope/snapshot/archive (D2, v11) — a custom
27
37
  * root deployment (review/tool-skill-manage write another tree) must point
28
38
  * the curator at the SAME tree or lifecycle decisions land on the default
29
39
  * tree and never see the deployed skills. Empty uses skillsRoot(). */
30
40
  root?: string;
31
41
  /** Spend one LLM review pass on stale candidates before the deterministic archive step. */
32
- llmReview?: boolean;
42
+ llmReview?: Volatile<boolean>;
33
43
  curatorProvider?: string;
34
44
  /** Quality-warned skills may turn stale after this many idle days. */
35
- qualityWarnStaleAfterDays?: number;
45
+ qualityWarnStaleAfterDays?: Volatile<number>;
36
46
  /** Skip automatic runs while any session was active within this many hours (0 disables). */
37
- minIdleHours?: number;
47
+ minIdleHours?: Volatile<number>;
38
48
  /** When true (default), a missing `agents` service is treated as "no active
39
49
  * session" (fail-open — the idle gate lets the run proceed); when false the
40
50
  * gate fails closed and defers the run until activity can be measured. */
41
- minIdleFailOpen?: boolean;
51
+ minIdleFailOpen?: Volatile<boolean>;
42
52
  /** Skill names excluded from the automated lifecycle. */
43
53
  excludeSkillNames?: string[];
44
54
  /** Include usage records whose created_by is not 'agent' in lifecycle decisions. */
@@ -52,15 +62,15 @@ export interface Config {
52
62
  /** Seconds between host boot and the first automatic schedule check (restart catch-up). */
53
63
  bootGraceSeconds?: number;
54
64
  /** Max tokens for the optional LLM nomination pass. */
55
- curatorReviewMaxTokens?: number;
65
+ curatorReviewMaxTokens?: Volatile<number>;
56
66
  /** Timeout (ms) for the optional LLM nomination pass (B-10, v18). */
57
- curatorReviewTimeoutMs?: number;
67
+ curatorReviewTimeoutMs?: Volatile<number>;
58
68
  /** Structure-health soft body limit (chars) — see DEFAULT_HEALTH_THRESHOLDS (rc.73 A1). */
59
- healthSoftBodyChars?: number;
69
+ healthSoftBodyChars?: Volatile<number>;
60
70
  /** Structure-health stamp-density ceiling per KB — see DEFAULT_HEALTH_THRESHOLDS. */
61
- healthStampDensityPerKb?: number;
71
+ healthStampDensityPerKb?: Volatile<number>;
62
72
  /** Structure-health write-ghost floor: patches at/above with zero reads (A2). */
63
- healthChurnMinPatches?: number;
73
+ healthChurnMinPatches?: Volatile<number>;
64
74
  }
65
75
  /** Curator behaviour a user may change (G3/S3.3). Field names are the CANONICAL
66
76
  * parameter ids from the registry, so the settings document, the params output,
@@ -91,10 +101,6 @@ export interface CuratorSettings {
91
101
  /** Patches without a read that flag a write-ghost skill. */
92
102
  healthChurnMinPatches: number;
93
103
  }
94
- /** Schema the platform validates the user layer against; defaults mirror the core
95
- * constants and the bounds mirror the row schema, so an empty document resolves to
96
- * today's behaviour and an out-of-range value is refused instead of clamped. */
97
- export declare const CURATOR_SETTINGS_SCHEMA: z<CuratorSettings>;
98
104
  /**
99
105
  * The cross-field rule the schema cannot express (A2-17): an archive threshold
100
106
  * below the stale threshold makes the engine reactivate stale records instead of
@@ -103,7 +109,7 @@ export declare const CURATOR_SETTINGS_SCHEMA: z<CuratorSettings>;
103
109
  * snapshot and the plugin row).
104
110
  * @param value - the resolved section the platform hands the owner.
105
111
  */
106
- export declare function validateCuratorSettings(value: CuratorSettings): void;
112
+ export declare function validateCuratorSettings(value: Pick<CuratorSettings, 'archiveAfterDays' | 'staleAfterDays'>): void;
107
113
  /** Outcome of one curator run pass. */
108
114
  export interface CuratorRunOutcome {
109
115
  stale: string[];
@@ -136,35 +142,69 @@ export declare function gateConsolidations(consolidations: CuratorConsolidation[
136
142
  }): CuratorConsolidation[];
137
143
  export declare class EvolutionCurator extends Service {
138
144
  static inject: string[];
139
- static Config: Schema<Config>;
145
+ static Config: z<Schemastery.ObjectS<NoInfer<{
146
+ root: z<string, string, "defined">;
147
+ enabled: z<boolean, boolean, "defined">;
148
+ curatorIntervalHours: z<number, number, "volatile">;
149
+ intervalHours: z<number, number, "plain">;
150
+ staleAfterDays: z<number, number, "volatile-defined">;
151
+ archiveAfterDays: z<number, number, "volatile-defined">;
152
+ llmReview: z<boolean, boolean, "volatile-defined">;
153
+ curatorProvider: z<string, string, "defined">;
154
+ qualityWarnStaleAfterDays: z<number, number, "volatile-defined">;
155
+ minIdleHours: z<number, number, "volatile-defined">;
156
+ minIdleFailOpen: z<boolean, boolean, "volatile-defined">;
157
+ excludeSkillNames: z<string[], string[], "defined">;
158
+ manageUnmanaged: z<boolean, boolean, "defined">;
159
+ pruneBuiltins: z<boolean, boolean, "defined">;
160
+ referencedSkillNames: z<string[], string[], "defined">;
161
+ autoStart: z<boolean, boolean, "defined">;
162
+ bootGraceSeconds: z<number, number, "defined">;
163
+ curatorReviewMaxTokens: z<number, number, "volatile-defined">;
164
+ curatorReviewTimeoutMs: z<number, number, "volatile-defined">;
165
+ healthSoftBodyChars: z<number, number, "volatile-defined">;
166
+ healthStampDensityPerKb: z<number, number, "volatile-defined">;
167
+ healthChurnMinPatches: z<number, number, "volatile-defined">;
168
+ }>>, Schemastery.ObjectT<NoInfer<{
169
+ root: z<string, string, "defined">;
170
+ enabled: z<boolean, boolean, "defined">;
171
+ curatorIntervalHours: z<number, number, "volatile">;
172
+ intervalHours: z<number, number, "plain">;
173
+ staleAfterDays: z<number, number, "volatile-defined">;
174
+ archiveAfterDays: z<number, number, "volatile-defined">;
175
+ llmReview: z<boolean, boolean, "volatile-defined">;
176
+ curatorProvider: z<string, string, "defined">;
177
+ qualityWarnStaleAfterDays: z<number, number, "volatile-defined">;
178
+ minIdleHours: z<number, number, "volatile-defined">;
179
+ minIdleFailOpen: z<boolean, boolean, "volatile-defined">;
180
+ excludeSkillNames: z<string[], string[], "defined">;
181
+ manageUnmanaged: z<boolean, boolean, "defined">;
182
+ pruneBuiltins: z<boolean, boolean, "defined">;
183
+ referencedSkillNames: z<string[], string[], "defined">;
184
+ autoStart: z<boolean, boolean, "defined">;
185
+ bootGraceSeconds: z<number, number, "defined">;
186
+ curatorReviewMaxTokens: z<number, number, "volatile-defined">;
187
+ curatorReviewTimeoutMs: z<number, number, "volatile-defined">;
188
+ healthSoftBodyChars: z<number, number, "volatile-defined">;
189
+ healthStampDensityPerKb: z<number, number, "volatile-defined">;
190
+ healthChurnMinPatches: z<number, number, "volatile-defined">;
191
+ }>>, "plain">;
140
192
  readonly skills: SkillLibrary;
141
193
  private readonly io;
142
194
  private readonly enabled;
143
- private readonly intervalHours;
144
- private readonly staleAfterDays;
145
- private readonly archiveAfterDays;
146
- private readonly llmReview;
195
+ /** This row's live config (G1 §8.3): its volatile fields are resolved at USE time. */
196
+ private readonly config;
147
197
  /** S1-C3: one warn per instance when `llmReview` is on but the llm service
148
198
  * is absent — the channel silently degrading must be visible at least once. */
149
199
  private llmAbsentWarned;
150
200
  private readonly curatorProvider;
151
- private readonly qualityWarnStaleAfterDays;
152
- private readonly minIdleHours;
153
- private readonly minIdleFailOpen;
154
201
  private readonly excludeSkillNames;
155
202
  private readonly manageUnmanaged;
156
203
  private readonly pruneBuiltins;
157
204
  private readonly referencedSkillNames;
158
205
  private readonly bootGraceSeconds;
159
- private readonly curatorReviewMaxTokens;
160
- private readonly curatorReviewTimeoutMs;
161
- private readonly healthSoftBodyChars;
162
- private readonly healthStampDensityPerKb;
163
- private readonly healthChurnMinPatches;
164
- /** G3/S3.3: this row's resolved values — the base layer of the settings section. */
165
- private readonly settingsBase;
166
- /** The user layer for this row's namespace (absent provider = deployment values). */
167
- private readonly overrides;
206
+ /** Keys whose supplied value had to be corrected; each warns once (G3.1). */
207
+ private readonly clampedKeys;
168
208
  private lastRun;
169
209
  private timer;
170
210
  /** B-8 (v18): set by the fiber disposer; a triggered autoCheck must not
@@ -192,14 +232,66 @@ export declare class EvolutionCurator extends Service {
192
232
  * yielding instance's log line names a concrete holder. */
193
233
  private readonly instanceOwner;
194
234
  constructor(ctx: Context, config?: Config);
235
+ /**
236
+ * Clamp one supplied numeric value, warning once per key (G3.1).
237
+ * @param key - the config key, for the warning.
238
+ * @param value - the value as supplied by the deployment or the user.
239
+ * @param fallback - the package default a corrected value falls back to.
240
+ * @param min - the smallest value the engine can act on.
241
+ * @param max - the largest value the carrier can hold, when bounded.
242
+ * @returns the value the engine may use.
243
+ */
244
+ private clampOnce;
245
+ /**
246
+ * One live numeric row field at USE time (G1 §8.3): the row config carries the
247
+ * platform-resolved layers (schema default < bundle row < profile override) and the
248
+ * field stays a live reference, so a committed edit applies without a watcher.
249
+ * @param key - the canonical registry id of the field.
250
+ * @returns the current plain value, or undefined when nothing supplied one.
251
+ */
252
+ private liveNumber;
253
+ /**
254
+ * One live numeric row field, clamped and warned once (G3.1).
255
+ * @param key - the canonical registry id of the field.
256
+ * @param fallback - the package default a corrected value falls back to.
257
+ * @param min - the smallest value the engine can act on.
258
+ * @param max - the largest value the carrier can hold, when bounded.
259
+ * @returns the value the engine may use.
260
+ */
261
+ private rowNumber;
262
+ /**
263
+ * One live boolean row field at USE time.
264
+ * @param key - the canonical registry id of the field.
265
+ * @param fallback - the value used when the deployment supplied none.
266
+ * @returns the current value.
267
+ */
268
+ private rowBoolean;
269
+ /**
270
+ * This row's resolved values — the base layer of the precedence chain (G1 §8.3).
271
+ * Resolved per call so a user edit is live; every numeric field is clamped.
272
+ * @returns the row layer of the curator's behaviour knobs.
273
+ */
274
+ private rowSettings;
275
+ /**
276
+ * The keys the user set in this row's namespace (G1 §8.4-A).
277
+ *
278
+ * This is the ONLY remaining read of the settings user layer, and it reads KEY
279
+ * NAMES, never values: the platform resolves the user layer into the row config,
280
+ * so a value is needed from `config` — but the precedence rule (user > policy)
281
+ * needs to know WHICH keys the user set, or a deployment policy would silently
282
+ * override the user.
283
+ * @returns the keys present in this row's user layer.
284
+ */
285
+ private userSetKeys;
195
286
  /** The policy-snapshot carrier for this group (unmounted service = no policy). */
196
287
  private policyFields;
197
288
  /**
198
- * G3/S3.3: the ONE reader of the curator's behaviour knobs. Precedence is
199
- * user > policy snapshot > this row, and {@link ParamOverrides.get} returns
200
- * undefined for a key the user never set, so the deployment carriers keep
201
- * winning exactly as before. Read at USE time: a committed settings change is
202
- * live without a restart and without a watcher.
289
+ * G3/S3.3 + G1: the ONE reader of the curator's behaviour knobs. Precedence is
290
+ * user > policy snapshot > this row: the row layer already carries the user value
291
+ * (a live field), and {@link userSetKeys} answers whether the user set the key —
292
+ * so an untouched key keeps following the deployment carriers exactly as before.
293
+ * Read at USE time: a committed settings change is live without a restart and
294
+ * without a watcher.
203
295
  * @returns the resolved section, every numeric field sanitized.
204
296
  */
205
297
  private settings;
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.16.0",
4
+ "version": "0.17.0-rc.1",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -27,20 +27,20 @@
27
27
  "license": "MIT",
28
28
  "dependencies": {
29
29
  "@deepseek-ai/schemastery": "^3.18.1",
30
- "@lmzhen/dsh-evolution-core": "^0.16.0"
30
+ "@lmzhen/dsh-evolution-core": "^0.17.0-rc.1"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "@deepseek-ai/cordis": "^4.0.1",
34
- "@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
35
- "@deepseek-ai/dsh-session": "^0.1.5-rc.2",
36
- "@lmzhen/dsh-evolution-io": "^0.16.0",
37
- "@lmzhen/dsh-evolution-state": "^0.16.0"
34
+ "@deepseek-ai/dsh-llm": "^0.2.0-rc.1",
35
+ "@deepseek-ai/dsh-session": "^0.2.0-rc.1",
36
+ "@lmzhen/dsh-evolution-io": "^0.17.0-rc.1",
37
+ "@lmzhen/dsh-evolution-state": "^0.17.0-rc.1"
38
38
  },
39
39
  "devDependencies": {
40
- "@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
41
- "@deepseek-ai/dsh-session": "^0.1.5-rc.2",
42
- "@lmzhen/dsh-evolution-core": "^0.16.0",
43
- "@lmzhen/dsh-evolution-io": "^0.16.0",
44
- "@lmzhen/dsh-evolution-state": "^0.16.0"
40
+ "@deepseek-ai/dsh-llm": "^0.2.0-rc.1",
41
+ "@deepseek-ai/dsh-session": "^0.2.0-rc.1",
42
+ "@lmzhen/dsh-evolution-core": "^0.17.0-rc.1",
43
+ "@lmzhen/dsh-evolution-io": "^0.17.0-rc.1",
44
+ "@lmzhen/dsh-evolution-state": "^0.17.0-rc.1"
45
45
  }
46
46
  }