@lmzhen/dsh-evolution-curator 0.3.17 → 0.3.18

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
@@ -32,6 +32,7 @@ var EvolutionCurator = class extends Service {
32
32
  curatorProvider: z.string().default("deepseek-official"),
33
33
  qualityWarnStaleAfterDays: z.number().default(DEFAULT_QUALITY_WARN_STALE_AFTER_DAYS),
34
34
  minIdleHours: z.number().default(DEFAULT_MIN_IDLE_HOURS),
35
+ minIdleFailOpen: z.boolean().default(true),
35
36
  excludeSkillNames: z.array(z.string()).default([]),
36
37
  manageUnmanaged: z.boolean().default(false),
37
38
  pruneBuiltins: z.boolean().default(false),
@@ -53,6 +54,7 @@ var EvolutionCurator = class extends Service {
53
54
  curatorProvider;
54
55
  qualityWarnStaleAfterDays;
55
56
  minIdleHours;
57
+ minIdleFailOpen;
56
58
  excludeSkillNames;
57
59
  manageUnmanaged;
58
60
  pruneBuiltins;
@@ -66,6 +68,10 @@ var EvolutionCurator = class extends Service {
66
68
  timer;
67
69
  bootCheck;
68
70
  running = false;
71
+ /** 0.3.18 (E-18): stateless first-run defer fires ONCE per process — the
72
+ * in-memory clock is seeded and later due runs must proceed, or the
73
+ * persisted===null defer repeats forever (no state service to persist). */
74
+ statelessFirstRunDeferred = false;
69
75
  constructor(ctx, config = {}) {
70
76
  super(ctx, "evolutionCurator");
71
77
  this.io = evolutionIoAdapter(() => ctx.evolutionIo.provider());
@@ -80,6 +86,7 @@ var EvolutionCurator = class extends Service {
80
86
  this.curatorProvider = config.curatorProvider ?? "deepseek-official";
81
87
  this.qualityWarnStaleAfterDays = config.qualityWarnStaleAfterDays ?? DEFAULT_QUALITY_WARN_STALE_AFTER_DAYS;
82
88
  this.minIdleHours = config.minIdleHours ?? DEFAULT_MIN_IDLE_HOURS;
89
+ this.minIdleFailOpen = config.minIdleFailOpen ?? true;
83
90
  this.excludeSkillNames = new Set(config.excludeSkillNames ?? []);
84
91
  this.manageUnmanaged = config.manageUnmanaged ?? false;
85
92
  this.pruneBuiltins = config.pruneBuiltins ?? false;
@@ -134,15 +141,13 @@ var EvolutionCurator = class extends Service {
134
141
  * kept deliberately: an unattended resume must not auto-run mid-boot).
135
142
  */
136
143
  async setPaused(paused) {
137
- const stateService = this.curatorStateService();
138
- const persisted = await stateService?.loadCuratorState() ?? null;
139
- await stateService?.saveCuratorState({
144
+ await this.curatorStateService()?.transactCuratorState((current) => ({
140
145
  schemaVersion: 1,
141
- lastRunAt: persisted?.lastRunAt ?? Date.now(),
142
- runCount: persisted?.runCount ?? 0,
143
- lastSummary: persisted?.lastSummary ?? (paused ? "paused" : "resumed"),
146
+ lastRunAt: current?.lastRunAt ?? Date.now(),
147
+ runCount: current?.runCount ?? 0,
148
+ lastSummary: current?.lastSummary ?? (paused ? "paused" : "resumed"),
144
149
  paused
145
- });
150
+ }));
146
151
  }
147
152
  /** Current persisted curator state (read-only view for /evolution curator status). */
148
153
  async status() {
@@ -156,8 +161,24 @@ var EvolutionCurator = class extends Service {
156
161
  * wake it, and never duplicates gate logic.
157
162
  */
158
163
  async autoCheck() {
159
- const last = (await this.curatorStateService()?.loadCuratorState())?.lastRunAt ?? this.lastRun;
160
- if (Date.now() - last >= this.lifecycle().intervalHours * 36e5) await this.run();
164
+ try {
165
+ const last = (await this.curatorStateService()?.loadCuratorState())?.lastRunAt ?? this.lastRun;
166
+ if (Date.now() - last >= this.lifecycle().intervalHours * 36e5) await this.run();
167
+ } catch (error) {
168
+ const reason = error instanceof Error ? error.message : String(error);
169
+ this.ctx.logger.warn(`evolution-curator: automatic check failed: ${reason}`);
170
+ try {
171
+ const runId = randomUUID();
172
+ await this.io.writeText(join(evolutionHome(), "reports", `curator-error-${runId}.json`), JSON.stringify({
173
+ runId,
174
+ failed: true,
175
+ error: reason,
176
+ at: (/* @__PURE__ */ new Date()).toISOString()
177
+ }, null, 2));
178
+ } catch (persistenceError) {
179
+ this.ctx.logger.warn(`evolution-curator: failed to persist auto-check error report: ${persistenceError instanceof Error ? persistenceError.message : String(persistenceError)}`);
180
+ }
181
+ }
161
182
  }
162
183
  /**
163
184
  * Optional Hermes-curator LLM pass. The model may only NOMINATE pruning and
@@ -173,14 +194,14 @@ var EvolutionCurator = class extends Service {
173
194
  if (candidates.length === 0) return empty;
174
195
  const llm = this.ctx.get("llm");
175
196
  if (!llm) return empty;
176
- const model = this.ctx.get("evolutionPolicy")?.get().curatorModel ?? "deepseek-v4-pro";
197
+ const model = this.ctx.get("evolutionPolicy")?.get()?.curatorModel ?? "deepseek-v4-pro";
177
198
  const clusters = computePrefixClusters(candidates);
178
199
  const clusterLines = clusters.length === 0 ? ["Prefix clusters observed in the candidate list: (none)"] : ["Prefix clusters observed in the candidate list (orientation only — verify against the names above; you may also flag additional clusters):", ...clusters.map((cluster) => `- '${cluster.key}': ${cluster.members.join(", ")}`)];
179
200
  const prompt = [
180
201
  options.dryRun ? CURATOR_DRY_RUN_BANNER : "",
181
202
  CURATOR_PROMPT,
182
203
  "",
183
- `Stale candidates observed by the deterministic lifecycle scanner:${candidates.length === 0 ? " (none)" : ""}`,
204
+ "Stale candidates observed by the deterministic lifecycle scanner:",
184
205
  ...candidates.map((name) => `- ${name}`),
185
206
  "",
186
207
  ...clusterLines,
@@ -211,7 +232,8 @@ var EvolutionCurator = class extends Service {
211
232
  prunings: parsed.prunings.filter((name) => candidates.includes(name)),
212
233
  consolidations: parsed.consolidations.filter((item) => candidates.includes(item.from))
213
234
  };
214
- } catch {
235
+ } catch (error) {
236
+ this.ctx.logger.warn(`evolution-curator: LLM nomination pass failed: ${error instanceof Error ? error.message : String(error)}`);
215
237
  return empty;
216
238
  }
217
239
  }
@@ -320,20 +342,24 @@ var EvolutionCurator = class extends Service {
320
342
  report: this.skippedReport(runId, startedAt),
321
343
  skipped: "active-session"
322
344
  };
323
- if (!ignoreGates && persisted === null) {
324
- await stateService?.saveCuratorState({
345
+ if (!ignoreGates && persisted === null && (stateService !== void 0 || !this.statelessFirstRunDeferred)) {
346
+ if (stateService) await stateService.saveCuratorState({
325
347
  schemaVersion: 1,
326
348
  lastRunAt: Date.now(),
327
349
  runCount: 0,
328
350
  lastSummary: "first-run-deferred",
329
351
  paused: false
330
352
  });
353
+ else {
354
+ this.lastRun = Date.now();
355
+ this.statelessFirstRunDeferred = true;
356
+ }
331
357
  return {
332
358
  stale: [],
333
359
  archived: [],
334
360
  errors: [],
335
361
  report: this.skippedReport(runId, startedAt),
336
- skipped: "first-run-deferred"
362
+ skipped: stateService ? "first-run-deferred" : "first-run-deferred(stateless)"
337
363
  };
338
364
  }
339
365
  const root = this.skills.root;
@@ -420,13 +446,15 @@ var EvolutionCurator = class extends Service {
420
446
  }
421
447
  const llmHint = !this.llmReview && result.markStale.length > 0 ? " (llmReview: off - deterministic archive only; set llmReview: true for the LLM merge channel)" : "";
422
448
  const summary = `${dryRun ? "dry-run" : "auto"}: stale:${result.markStale.length} archived:${archivedSkills.length} consolidated:${gatedNominations.consolidations.length}${llmHint}`;
423
- const pausedNow = (await stateService?.loadCuratorState())?.paused ?? false;
424
- await stateService?.saveCuratorState({
425
- schemaVersion: 1,
426
- lastRunAt: dryRun ? persisted?.lastRunAt ?? this.lastRun : this.lastRun,
427
- runCount: dryRun ? persisted?.runCount ?? 0 : (persisted?.runCount ?? 0) + 1,
428
- lastSummary: summary,
429
- paused: pausedNow
449
+ await stateService?.transactCuratorState((current) => {
450
+ const pausedNow = current?.paused ?? false;
451
+ return {
452
+ schemaVersion: 1,
453
+ lastRunAt: dryRun ? persisted?.lastRunAt ?? this.lastRun : Date.now(),
454
+ runCount: dryRun ? persisted?.runCount ?? 0 : (current?.runCount ?? 0) + 1,
455
+ lastSummary: summary,
456
+ paused: pausedNow
457
+ };
430
458
  });
431
459
  return {
432
460
  stale: result.markStale,
@@ -511,6 +539,15 @@ var EvolutionCurator = class extends Service {
511
539
  const suppressedAdded = /* @__PURE__ */ new Set();
512
540
  const stateOwned = new Set(input.stateOwned ?? []);
513
541
  for (const name of archiveCandidates) {
542
+ if (!treeNames.has(name)) {
543
+ const record = usage.get(name);
544
+ if (record) {
545
+ record.state = "archived";
546
+ record.archived_at = record.archived_at ?? (/* @__PURE__ */ new Date()).toISOString();
547
+ stateOwned.add(name);
548
+ }
549
+ continue;
550
+ }
514
551
  const archived = await this.skills.archive(name, {
515
552
  reason: "Lifecycle: reached archive threshold",
516
553
  allowBundled: this.pruneBuiltins
@@ -603,7 +640,7 @@ var EvolutionCurator = class extends Service {
603
640
  }
604
641
  recentSessionActive() {
605
642
  const agents = this.ctx.get("agents");
606
- if (!agents) return false;
643
+ if (!agents) return !this.minIdleFailOpen;
607
644
  let latest = 0;
608
645
  for (const agent of agents.list()) {
609
646
  const events = agent.session.events;
@@ -650,20 +687,15 @@ var EvolutionCurator = class extends Service {
650
687
  }
651
688
  async latestReport() {
652
689
  const reportsRoot = join(evolutionHome(), "reports");
653
- const names = (await this.io.list(reportsRoot)).filter((name) => name.startsWith("curator-") && name.endsWith(".json")).sort();
690
+ const names = (await this.io.list(reportsRoot)).filter((name) => name.startsWith("curator-") && name.endsWith(".json"));
654
691
  let latest = null;
655
692
  for (const name of names) {
656
- const raw = await this.io.readText(join(reportsRoot, name));
657
- if (raw === null) continue;
658
- try {
659
- const parsed = JSON.parse(raw);
660
- const startedAt = typeof parsed.startedAt === "string" ? Date.parse(parsed.startedAt) : 0;
661
- if (!Number.isFinite(startedAt)) continue;
662
- if (latest === null || startedAt > latest.startedAt) latest = {
663
- name,
664
- startedAt
665
- };
666
- } catch {}
693
+ const mtime = await this.io.mtime?.(join(reportsRoot, name)) ?? null;
694
+ if (mtime === null) continue;
695
+ if (latest === null || mtime > latest.mtime) latest = {
696
+ name,
697
+ mtime
698
+ };
667
699
  }
668
700
  if (latest === null) return null;
669
701
  const raw = await this.io.readText(join(reportsRoot, latest.name));
@@ -6,6 +6,7 @@ import { Context, Service } from '@deepseek-ai/cordis';
6
6
  import type Schema from '@deepseek-ai/schemastery';
7
7
  import { EvolutionGateSet, SkillLibrary } from '@deepseek-ai/dsh-evolution-core';
8
8
  import { type CuratorConsolidation, type CuratorNominations, type CuratorRunReport, type ScopeView, type SkillActionResult, type SkillHealthVerdict } from '@deepseek-ai/dsh-evolution-core';
9
+ import type { CuratorStateRecord } from '@deepseek-ai/dsh-evolution-state';
9
10
  declare module '@deepseek-ai/cordis' {
10
11
  interface Context {
11
12
  evolutionCurator: EvolutionCurator;
@@ -23,6 +24,10 @@ export interface Config {
23
24
  qualityWarnStaleAfterDays?: number;
24
25
  /** Skip automatic runs while any session was active within this many hours (0 disables). */
25
26
  minIdleHours?: number;
27
+ /** When true (default), a missing `agents` service is treated as "no active
28
+ * session" (fail-open — the idle gate lets the run proceed); when false the
29
+ * gate fails closed and defers the run until activity can be measured. */
30
+ minIdleFailOpen?: boolean;
26
31
  /** Skill names excluded from the automated lifecycle. */
27
32
  excludeSkillNames?: string[];
28
33
  /** Include usage records whose created_by is not 'agent' in lifecycle decisions. */
@@ -54,14 +59,15 @@ export interface CuratorRunOutcome {
54
59
  /** LLM nominations when the optional review pass is enabled (audit visibility). */
55
60
  nominations?: CuratorNominations;
56
61
  }
57
- /** Persisted curator-state record shape (schemaVersion optional for legacy reads). */
58
- export interface CuratorStateRecordShape {
62
+ /**
63
+ * Persisted curator-state record. The base fields are the authoritative
64
+ * `CuratorStateRecord` from evolution-state-storage; `schemaVersion` is an
65
+ * optional on-disk shape marker the storage seam leaves untyped (kept for
66
+ * legacy reads — 0.3.18, E-53).
67
+ */
68
+ export type CuratorStateRecordShape = CuratorStateRecord & {
59
69
  schemaVersion?: number;
60
- lastRunAt: number;
61
- runCount: number;
62
- lastSummary: string;
63
- paused: boolean;
64
- }
70
+ };
65
71
  /**
66
72
  * Block LLM-nominated consolidations that would touch a gate-protected name:
67
73
  * exclude / referenced / suppressed skills must never merge (neither as the
@@ -86,6 +92,7 @@ export declare class EvolutionCurator extends Service {
86
92
  private readonly curatorProvider;
87
93
  private readonly qualityWarnStaleAfterDays;
88
94
  private readonly minIdleHours;
95
+ private readonly minIdleFailOpen;
89
96
  private readonly excludeSkillNames;
90
97
  private readonly manageUnmanaged;
91
98
  private readonly pruneBuiltins;
@@ -99,6 +106,10 @@ export declare class EvolutionCurator extends Service {
99
106
  private timer;
100
107
  private bootCheck;
101
108
  private running;
109
+ /** 0.3.18 (E-18): stateless first-run defer fires ONCE per process — the
110
+ * in-memory clock is seeded and later due runs must proceed, or the
111
+ * persisted===null defer repeats forever (no state service to persist). */
112
+ private statelessFirstRunDeferred;
102
113
  constructor(ctx: Context, config?: Config);
103
114
  private lifecycle;
104
115
  start(): void;
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.17",
4
+ "version": "0.3.18",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -33,20 +33,20 @@
33
33
  "license": "MIT",
34
34
  "dependencies": {
35
35
  "@deepseek-ai/schemastery": "^3.18.1",
36
- "@lmzhen/dsh-evolution-core": "^0.3.17"
36
+ "@lmzhen/dsh-evolution-core": "^0.3.18"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@deepseek-ai/cordis": "^4.0.1",
40
40
  "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
41
41
  "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
42
- "@lmzhen/dsh-evolution-io": "^0.3.17",
43
- "@lmzhen/dsh-evolution-state": "^0.3.17"
42
+ "@lmzhen/dsh-evolution-io": "^0.3.18",
43
+ "@lmzhen/dsh-evolution-state": "^0.3.18"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
47
47
  "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
48
- "@lmzhen/dsh-evolution-core": "^0.3.17",
49
- "@lmzhen/dsh-evolution-io": "^0.3.17",
50
- "@lmzhen/dsh-evolution-state": "^0.3.17"
48
+ "@lmzhen/dsh-evolution-core": "^0.3.18",
49
+ "@lmzhen/dsh-evolution-io": "^0.3.18",
50
+ "@lmzhen/dsh-evolution-state": "^0.3.18"
51
51
  }
52
52
  }