@lmzhen/dsh-evolution-approval 0.20.0 → 0.21.0

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
@@ -1,6 +1,6 @@
1
1
  import { Service } from "@deepseek-ai/cordis";
2
2
  import { randomUUID } from "node:crypto";
3
- import { elapsedSince, isLoopbackRequest, isProcessAlive, parseLockBody, readJsonObject, textDiffFacts, writeJson } from "@lmzhen/dsh-evolution-core";
3
+ import { clampOnce, elapsedSince, isLoopbackRequest, isProcessAlive, parseLockBody, readJsonObject, textDiffFacts, writeJson } from "@lmzhen/dsh-evolution-core";
4
4
  import z from "@deepseek-ai/schemastery";
5
5
  import { PENDING_STATUSES } from "@lmzhen/dsh-evolution-state-storage";
6
6
  //#region lib/types/routes.js
@@ -242,13 +242,15 @@ var EvolutionApproval = class extends Service {
242
242
  static Config = z.object({
243
243
  enabled: z.boolean().default(false),
244
244
  stageForeground: z.boolean().default(true),
245
- pendingTtlMs: z.number().min(0).default(0)
245
+ pendingTtlMs: z.number().min(0).default(0).volatile()
246
246
  });
247
247
  enabled;
248
248
  stageForegroundConfig;
249
- /** Effective TTL in ms; 0 = the window never expires (T4-09/A51). A non-finite or negative value
250
- * is clamped here because the schema only guards the loader path. */
251
- pendingTtlMs;
249
+ /** The TTL carrier as configured: a plain number, or the live reference `.volatile()` hands a
250
+ * user-layer field. Read through {@link ttl} at every use, never cached (registry row: live). */
251
+ pendingTtlMsConfig;
252
+ /** Keys this instance already warned about (clampOnce's once-per-key sink). */
253
+ clampedKeys = /* @__PURE__ */ new Set();
252
254
  /** P3 (v16): public read for staging pre-checks (learning-graph refuses a
253
255
  * stage that no runner could replay only when foreground writes stage at
254
256
  * all). Mirrors `this.stageForeground`. */
@@ -262,8 +264,13 @@ var EvolutionApproval = class extends Service {
262
264
  super(ctx, "evolutionApproval");
263
265
  this.enabled = config.enabled ?? false;
264
266
  this.stageForegroundConfig = config.stageForeground ?? true;
265
- const ttl = config.pendingTtlMs ?? 0;
266
- this.pendingTtlMs = Number.isFinite(ttl) && ttl > 0 ? Math.floor(ttl) : 0;
267
+ this.pendingTtlMsConfig = config.pendingTtlMs;
268
+ this.ctx.inject(["settings"], (injected) => {
269
+ const settings = injected.settings;
270
+ if (typeof settings?.configure !== "function") return;
271
+ const disposer = settings.configure({ auto: false }, this.ctx.fiber);
272
+ if (typeof disposer === "function") this.ctx.effect(() => disposer, "evolution-approval: settings presentation");
273
+ });
267
274
  this.ctx.inject(["webServer"], (scope) => {
268
275
  scope.effect(() => {
269
276
  const disposers = makeApprovalRoutes(this).map((route) => scope.webServer.register(route));
@@ -360,7 +367,22 @@ var EvolutionApproval = class extends Service {
360
367
  /** T4-09/A51: the effective expiry of the staged window, 0 = never. Read by the command surface
361
368
  * and the doctor so both can say which of the three human-confirmation states is in force. */
362
369
  get ttlMs() {
363
- return this.pendingTtlMs;
370
+ return this.ttl();
371
+ }
372
+ /**
373
+ * The effective TTL in ms (0 = never expire), read at USE time.
374
+ *
375
+ * `clampOnce` is the family's ONE clamp-and-warn helper for a numeric knob that may be a live
376
+ * settings reference (S2.9/T3-10): a plain number, a `{ get() }` reference, or nothing all land
377
+ * on the same floor, and an invalid value warns once per instance instead of silently disabling
378
+ * the window. The old inline `Number.isFinite` read stopped working the moment the field became
379
+ * `.volatile()` — a reference is not a finite number, so the TTL silently read as 0.
380
+ * @returns the TTL the sweep may use.
381
+ */
382
+ ttl() {
383
+ return Math.floor(clampOnce(this.clampedKeys, (message) => {
384
+ this.ctx.logger.warn(message);
385
+ }, "pendingTtlMs", this.pendingTtlMsConfig, 0, 0));
364
386
  }
365
387
  /**
366
388
  * Close every staged write that has been waiting longer than the TTL (T4-09/A51).
@@ -379,15 +401,16 @@ var EvolutionApproval = class extends Service {
379
401
  * @returns the ids that were closed by this sweep (empty when the TTL is off or nothing was stale).
380
402
  */
381
403
  async expireStale() {
382
- if (this.pendingTtlMs <= 0) return [];
383
- const cutoff = Date.now() - this.pendingTtlMs;
404
+ const ttl = this.ttl();
405
+ if (ttl <= 0) return [];
406
+ const cutoff = Date.now() - ttl;
384
407
  const expired = [];
385
408
  for (const record of await this.state().listPending("pending")) {
386
409
  const stagedAt = Date.parse(record.createdAt);
387
410
  if (!Number.isFinite(stagedAt) || stagedAt > cutoff) continue;
388
411
  if ((await this.state().tryResolvePending(record.id, "rejected")).applied) expired.push(record.id);
389
412
  }
390
- if (expired.length > 0) this.ctx.logger.warn(`evolution-approval: ${expired.length} staged write(s) passed pendingTtlMs=${this.pendingTtlMs} and were closed as rejected WITHOUT executing: ${expired.join(", ")} — stage them again if they are still wanted`);
413
+ if (expired.length > 0) this.ctx.logger.warn(`evolution-approval: ${expired.length} staged write(s) passed pendingTtlMs=${ttl} and were closed as rejected WITHOUT executing: ${expired.join(", ")} — stage them again if they are still wanted`);
391
414
  return expired;
392
415
  }
393
416
  async list(status = "pending") {
@@ -17,7 +17,8 @@
17
17
  * @module @lmzhen/dsh-evolution-approval
18
18
  */
19
19
  import { Context, Service } from '@deepseek-ai/cordis';
20
- import type Schema from '@deepseek-ai/schemastery';
20
+ import type { Volatile } from '@deepseek-ai/cordis';
21
+ import z from '@deepseek-ai/schemastery';
21
22
  import type { PendingKind, PendingRecord, PendingStatus } from '@lmzhen/dsh-evolution-state-storage';
22
23
  export type { PendingKind, PendingRecord, PendingStatus };
23
24
  export type WriteRunner = (args: unknown) => Promise<{
@@ -158,17 +159,30 @@ export interface Config {
158
159
  * the window then behaves exactly as before. Expiry closes the record the same way a reject does
159
160
  * (status `rejected`, `resolvedAt` stamped, runner NEVER invoked): an expiry is a cleanup of the
160
161
  * window, never an approval, so nothing a human did not approve can execute.
162
+ *
163
+ * `.volatile()` (0.21.0, G1): the field carries a USER LAYER, so the value arrives as a live
164
+ * reference and is read at USE time — a settings write changes the window without a reload.
161
165
  */
162
- pendingTtlMs?: number;
166
+ pendingTtlMs?: Volatile<number>;
163
167
  }
164
168
  export declare class EvolutionApproval extends Service {
165
169
  static inject: string[];
166
- static Config: Schema<Config>;
170
+ static Config: z<Schemastery.ObjectS<NoInfer<{
171
+ enabled: z<boolean, boolean, "defined">;
172
+ stageForeground: z<boolean, boolean, "defined">;
173
+ pendingTtlMs: z<number, number, "volatile-defined">;
174
+ }>>, Schemastery.ObjectT<NoInfer<{
175
+ enabled: z<boolean, boolean, "defined">;
176
+ stageForeground: z<boolean, boolean, "defined">;
177
+ pendingTtlMs: z<number, number, "volatile-defined">;
178
+ }>>, "plain">;
167
179
  private readonly enabled;
168
180
  private readonly stageForegroundConfig;
169
- /** Effective TTL in ms; 0 = the window never expires (T4-09/A51). A non-finite or negative value
170
- * is clamped here because the schema only guards the loader path. */
171
- private readonly pendingTtlMs;
181
+ /** The TTL carrier as configured: a plain number, or the live reference `.volatile()` hands a
182
+ * user-layer field. Read through {@link ttl} at every use, never cached (registry row: live). */
183
+ private readonly pendingTtlMsConfig?;
184
+ /** Keys this instance already warned about (clampOnce's once-per-key sink). */
185
+ private readonly clampedKeys;
172
186
  /** P3 (v16): public read for staging pre-checks (learning-graph refuses a
173
187
  * stage that no runner could replay only when foreground writes stage at
174
188
  * all). Mirrors `this.stageForeground`. */
@@ -209,6 +223,17 @@ export declare class EvolutionApproval extends Service {
209
223
  /** T4-09/A51: the effective expiry of the staged window, 0 = never. Read by the command surface
210
224
  * and the doctor so both can say which of the three human-confirmation states is in force. */
211
225
  get ttlMs(): number;
226
+ /**
227
+ * The effective TTL in ms (0 = never expire), read at USE time.
228
+ *
229
+ * `clampOnce` is the family's ONE clamp-and-warn helper for a numeric knob that may be a live
230
+ * settings reference (S2.9/T3-10): a plain number, a `{ get() }` reference, or nothing all land
231
+ * on the same floor, and an invalid value warns once per instance instead of silently disabling
232
+ * the window. The old inline `Number.isFinite` read stopped working the moment the field became
233
+ * `.volatile()` — a reference is not a finite number, so the TTL silently read as 0.
234
+ * @returns the TTL the sweep may use.
235
+ */
236
+ private ttl;
212
237
  /**
213
238
  * Close every staged write that has been waiting longer than the TTL (T4-09/A51).
214
239
  *
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-approval",
3
3
  "description": "Stage/pending approval service for Hermes-style self-evolution writes (community build)",
4
- "version": "0.20.0",
4
+ "version": "0.21.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -27,18 +27,18 @@
27
27
  "license": "MIT",
28
28
  "dependencies": {
29
29
  "@deepseek-ai/schemastery": "^3.18.4",
30
- "@lmzhen/dsh-evolution-core": "^0.20.0"
30
+ "@lmzhen/dsh-evolution-core": "^0.21.0"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "@deepseek-ai/cordis": "^4.0.1",
34
34
  "@deepseek-ai/dsh-host-webserver": "^0.2.0-rc.1",
35
- "@lmzhen/dsh-evolution-state-storage": "^0.20.0",
36
- "@lmzhen/dsh-evolution-state": "^0.20.0"
35
+ "@lmzhen/dsh-evolution-state-storage": "^0.21.0",
36
+ "@lmzhen/dsh-evolution-state": "^0.21.0"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@deepseek-ai/dsh-host-webserver": "^0.2.0-rc.1",
40
- "@lmzhen/dsh-evolution-core": "^0.20.0",
41
- "@lmzhen/dsh-evolution-state-storage": "^0.20.0",
42
- "@lmzhen/dsh-evolution-state": "^0.20.0"
40
+ "@lmzhen/dsh-evolution-core": "^0.21.0",
41
+ "@lmzhen/dsh-evolution-state-storage": "^0.21.0",
42
+ "@lmzhen/dsh-evolution-state": "^0.21.0"
43
43
  }
44
44
  }