@lmzhen/dsh-evolution-review 0.3.23 → 0.3.25

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 +2 -0
  2. package/lib/index.js +72 -33
  3. package/package.json +10 -10
package/README.md CHANGED
@@ -26,6 +26,8 @@ Independent of request-prefix construction. This package does not alter the asse
26
26
  - The review request text is redacted for credential-shaped patterns before it reaches the subagent, but redaction is pattern-based and best-effort, not a security boundary.
27
27
  - Read-before-write tracks only reads through the `skill` tool. `skill_manage` has no per-skill read action (`list`/`review` are whole-library, not targeted at one name), so a skill that was only listed via `skill_manage` is not marked as read — a background review may still reject a patch to it until it is actually loaded.
28
28
  - The completion-channel counters (`cumulativeToolCalls` / `completionInjected`) are in-memory only. A process restart resets them, which is accepted behavior: the completion review is a one-per-session post-task adaptation and a restart is treated as a fresh conversation boundary. The cadence state (`turnsSinceMemory` / `turnsSinceSkill`) is persisted via `ReviewState` and survives restart.
29
+ - `evolution/review-scheduled` and `evolution/review-error` are emitted for platform/user wiring only — this family has no in-repo production `ctx.on` consumer for them. They are declared externally owned (the platform side wires consumption), which matches the `EXEMPT_ORPHANS` set in `scripts/verify-event-pairing.mjs`.
30
+ - When the `evolution-state` service is not mounted, the memory/skill cadence state is not persisted and every turn restarts from a clean `{ turnsSinceMemory: 0, turnsSinceSkill: 0 }` baseline — the review schedule is stateless and re-decided each turn rather than accumulating across the conversation. The loss is surfaced once per process as a logger warning at the first turn/end.
29
31
 
30
32
  ## Configuration
31
33
 
package/lib/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
4
- import { COMPLETION_SKILL_REVIEW_PROMPT, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_USER_CHAR_LIMIT, PROMPT_BUNDLE, SkillLibrary, advanceReview, evolutionIoAdapter, foldTurn, redactSecrets, resolveOrigins, reviewPrompt, verifyPromptBundle } from "@lmzhen/dsh-evolution-core";
4
+ import { COMPLETION_SKILL_REVIEW_PROMPT, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_USER_CHAR_LIMIT, PROMPT_BUNDLE, SkillLibrary, advanceReview, clampedNumber, evolutionIoAdapter, foldTurn, redactSecrets, resolveOrigins, reviewPrompt, verifyPromptBundle } from "@lmzhen/dsh-evolution-core";
5
5
  import { validateEvolutionPlan } from "@lmzhen/dsh-evolution-plan-validator";
6
6
  //#region lib/types/index.js
7
7
  /**
@@ -13,21 +13,52 @@ const inject = ["agents"];
13
13
  const Config = z.object({
14
14
  reviewEnabled: z.boolean().default(true),
15
15
  reviewMode: z.union([z.const("subagent"), z.const("inject")]).default("subagent"),
16
- memoryInterval: z.number().default(DEFAULT_REVIEW_MEMORY_INTERVAL),
17
- skillInterval: z.number().default(DEFAULT_REVIEW_SKILL_INTERVAL),
16
+ memoryInterval: z.number().min(1).default(DEFAULT_REVIEW_MEMORY_INTERVAL),
17
+ skillInterval: z.number().min(1).default(DEFAULT_REVIEW_SKILL_INTERVAL),
18
18
  reviewToolAllow: z.array(z.string()).default(["skill"]),
19
- reviewTimeoutMs: z.number().default(12e4),
20
- executionTimeoutMs: z.number().default(3e4),
21
- reviewContextMessages: z.number().default(60),
22
- reviewMessageChars: z.number().default(2e3),
23
- reviewMaxDepth: z.number().default(1),
19
+ reviewTimeoutMs: z.number().min(1).default(12e4),
20
+ executionTimeoutMs: z.number().min(1).default(3e4),
21
+ reviewContextMessages: z.number().min(1).default(60),
22
+ reviewMessageChars: z.number().min(1).default(2e3),
23
+ reviewMaxDepth: z.number().min(1).default(1),
24
24
  reviewProvider: z.string(),
25
25
  skillReviewTrigger: z.string().default(DEFAULT_SKILL_REVIEW_TRIGGER),
26
- skillReviewCompletionMinToolCalls: z.number().default(DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS)
26
+ skillReviewCompletionMinToolCalls: z.number().min(1).default(DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS)
27
27
  });
28
+ let statelessReviewStateWarned = false;
29
+ /**
30
+ * G3.1 (0.3.23): clamp the numeric review config at assembly so a 0/negative/
31
+ * NaN/±Infinity value falls back to the package default instead of folding as a
32
+ * "disabled" special value (a 0 interval would fire a review every turn; NaN
33
+ * folds as NaN into the cadence/timeout). The schema `.min(1)` guards the
34
+ * loader path; this clamp also covers NaN/±Infinity (which schemastery lets a
35
+ * bare number schema through) and direct construction. `reviewMaxDepth` clamps
36
+ * to at least 1 because 0 is the historical 0.3.1 maximum-depth defect (a 0
37
+ * rejects the spawn outright). Warn once when a user-supplied value had to be
38
+ * corrected.
39
+ */
40
+ function clampReviewConfig(rawConfig, ctx) {
41
+ const clamped = [];
42
+ const field = (name, value, fallback, min) => {
43
+ const result = clampedNumber(value, fallback, { min });
44
+ if (value !== void 0 && result !== value) clamped.push(name);
45
+ return result;
46
+ };
47
+ const config = Object.assign({}, rawConfig, {
48
+ memoryInterval: field("memoryInterval", rawConfig.memoryInterval, DEFAULT_REVIEW_MEMORY_INTERVAL, 1),
49
+ skillInterval: field("skillInterval", rawConfig.skillInterval, DEFAULT_REVIEW_SKILL_INTERVAL, 1),
50
+ reviewTimeoutMs: field("reviewTimeoutMs", rawConfig.reviewTimeoutMs, 12e4, 1),
51
+ reviewContextMessages: field("reviewContextMessages", rawConfig.reviewContextMessages, 60, 1),
52
+ reviewMessageChars: field("reviewMessageChars", rawConfig.reviewMessageChars, 2e3, 1),
53
+ reviewMaxDepth: field("reviewMaxDepth", rawConfig.reviewMaxDepth, 1, 1),
54
+ skillReviewCompletionMinToolCalls: field("skillReviewCompletionMinToolCalls", rawConfig.skillReviewCompletionMinToolCalls, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, 1)
55
+ });
56
+ if (clamped.length > 0) ctx.logger.warn(`dsh-evolution-review: ${clamped.join(", ")} provided an invalid value; falling back to the default`);
57
+ return config;
58
+ }
28
59
  function apply(ctx, rawConfig) {
29
60
  if (!verifyPromptBundle(PROMPT_BUNDLE)) throw new Error("dsh-evolution prompt bundle integrity check failed; refusing to schedule review work");
30
- const config = rawConfig;
61
+ const config = clampReviewConfig(rawConfig, ctx);
31
62
  const turnStarts = /* @__PURE__ */ new Map();
32
63
  const cumulativeToolCalls = /* @__PURE__ */ new Map();
33
64
  const completionInjected = /* @__PURE__ */ new Set();
@@ -60,6 +91,10 @@ function apply(ctx, rawConfig) {
60
91
  const signal = foldTurn(session, turnStarts.get(session.id) ?? Math.max(0, session.seq - 1));
61
92
  turnStarts.delete(session.id);
62
93
  const stateService = ctx.get("evolutionState");
94
+ if (!stateService && !statelessReviewStateWarned) {
95
+ statelessReviewStateWarned = true;
96
+ ctx.logger.warn("dsh-evolution-review: evolution-state service not mounted — memory/skill review cadence is not persisted and resets every turn (see README Known Limitations).");
97
+ }
63
98
  const state = await stateService?.loadReviewState(session.id) ?? {
64
99
  turnsSinceMemory: 0,
65
100
  turnsSinceSkill: 0,
@@ -103,13 +138,6 @@ function apply(ctx, rawConfig) {
103
138
  if (completionInjected.has(session.id)) return;
104
139
  if (!shouldCompletionReview(event.data.reason, cumulative, config.skillReviewCompletionMinToolCalls)) return;
105
140
  completionInjected.add(session.id);
106
- ctx.emit("evolution/review-scheduled", {
107
- sessionId: session.id,
108
- kind: "skill",
109
- toolCalls: signal.toolCalls,
110
- userChars: signal.userChars,
111
- assistantChars: signal.assistantChars
112
- });
113
141
  agent.inject(createUserMessage({
114
142
  content: [{
115
143
  type: "text",
@@ -122,6 +150,13 @@ function apply(ctx, rawConfig) {
122
150
  summary: "completion review"
123
151
  }
124
152
  }));
153
+ ctx.emit("evolution/review-scheduled", {
154
+ sessionId: session.id,
155
+ kind: "skill",
156
+ toolCalls: signal.toolCalls,
157
+ userChars: signal.userChars,
158
+ assistantChars: signal.assistantChars
159
+ });
125
160
  }
126
161
  async function trySubagentReview(session, agent, kind, signal) {
127
162
  if ((policy()?.reviewMode ?? config.reviewMode) === "inject") return false;
@@ -186,6 +221,26 @@ function apply(ctx, rawConfig) {
186
221
  const executed = await executePlan(validation.accepted, session.id);
187
222
  const actions = executed.actions;
188
223
  const evidenceQuotes = [...validation.accepted.memoryOps ?? [], ...acceptedSkillOps].reduce((total, op) => total + (Array.isArray(op.evidence) ? op.evidence.length : 0), 0);
224
+ if (actions.length > 0) {
225
+ const applied = actions.join(" · ");
226
+ const note = executed.ok ? "" : "\n部分操作失败。以下操作已应用,请勿重复执行。";
227
+ try {
228
+ agent.inject(createUserMessage({
229
+ content: [{
230
+ type: "text",
231
+ text: `💾 Self-improvement review: ${applied}${note}`
232
+ }],
233
+ source: {
234
+ kind: "plugin",
235
+ plugin: "dsh-evolution-review",
236
+ form: "notice",
237
+ summary: "self-improvement review"
238
+ }
239
+ }));
240
+ } catch (injectError) {
241
+ ctx.logger.warn(`dsh-evolution-review: result notice inject failed: ${injectError instanceof Error ? injectError.message : String(injectError)}`);
242
+ }
243
+ }
189
244
  ctx.emit("evolution/plan-applied", {
190
245
  sessionId: session.id,
191
246
  planId: randomUUID(),
@@ -196,22 +251,6 @@ function apply(ctx, rawConfig) {
196
251
  evidenceQuotes,
197
252
  estimatedInputChars: reviewText.length
198
253
  });
199
- if (actions.length > 0) {
200
- const applied = actions.join(" · ");
201
- const note = executed.ok ? "" : "\n部分操作失败。以下操作已应用,请勿重复执行。";
202
- agent.inject(createUserMessage({
203
- content: [{
204
- type: "text",
205
- text: `💾 Self-improvement review: ${applied}${note}`
206
- }],
207
- source: {
208
- kind: "plugin",
209
- plugin: "dsh-evolution-review",
210
- form: "notice",
211
- summary: "self-improvement review"
212
- }
213
- }));
214
- }
215
254
  return true;
216
255
  } finally {
217
256
  try {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-review",
3
3
  "description": "Background review orchestration (community build)",
4
- "version": "0.3.23",
4
+ "version": "0.3.25",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -31,9 +31,9 @@
31
31
  "license": "MIT",
32
32
  "dependencies": {
33
33
  "@deepseek-ai/schemastery": "^3.18.1",
34
- "@lmzhen/dsh-evolution-approval": "^0.3.23",
35
- "@lmzhen/dsh-evolution-core": "^0.3.23",
36
- "@lmzhen/dsh-evolution-plan-validator": "^0.3.23"
34
+ "@lmzhen/dsh-evolution-approval": "^0.3.25",
35
+ "@lmzhen/dsh-evolution-core": "^0.3.25",
36
+ "@lmzhen/dsh-evolution-plan-validator": "^0.3.25"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@deepseek-ai/cordis": "^4.0.1",
@@ -42,8 +42,8 @@
42
42
  "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
43
43
  "@deepseek-ai/dsh-session": "^0.1.1-rc.2",
44
44
  "@deepseek-ai/dsh-tools": "^0.1.1-rc.2",
45
- "@lmzhen/dsh-evolution-state": "^0.3.23",
46
- "@lmzhen/dsh-evolution-policy": "^0.3.23"
45
+ "@lmzhen/dsh-evolution-state": "^0.3.25",
46
+ "@lmzhen/dsh-evolution-policy": "^0.3.25"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@deepseek-ai/dsh-agent": "^0.1.1-rc.2",
@@ -54,9 +54,9 @@
54
54
  "@deepseek-ai/dsh-session-persistence": "^0.1.1-rc.2",
55
55
  "@deepseek-ai/dsh-session-persistence-jsonl": "^0.1.1-rc.2",
56
56
  "@deepseek-ai/dsh-tools": "^0.1.1-rc.2",
57
- "@lmzhen/dsh-evolution-approval": "^0.3.23",
58
- "@lmzhen/dsh-evolution-core": "^0.3.23",
59
- "@lmzhen/dsh-evolution-plan-validator": "^0.3.23",
60
- "@lmzhen/dsh-evolution-state": "^0.3.23"
57
+ "@lmzhen/dsh-evolution-approval": "^0.3.25",
58
+ "@lmzhen/dsh-evolution-core": "^0.3.25",
59
+ "@lmzhen/dsh-evolution-plan-validator": "^0.3.25",
60
+ "@lmzhen/dsh-evolution-state": "^0.3.25"
61
61
  }
62
62
  }