@lmzhen/dsh-evolution-review 0.3.17 → 0.3.19

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/README.md CHANGED
@@ -21,9 +21,11 @@ Independent of request-prefix construction. This package does not alter the asse
21
21
  ## Known Limitations and Deferred Work
22
22
 
23
23
 
24
- - Review subagents inherit the host preset; Anchored Standard deployments rely on the default `skill_search`/`skill_load` allow-list.
24
+ - Review subagents are spawned with the plain `skill` tool only (`reviewToolAllow` default and the host/preset config both = `[skill]` the DSH tool catalog has no `skill_search`/`skill_load` discovery pair, so the Hermes-lineage Anchored Standard `skill_search`/`skill_load` allow-list does not exist here).
25
25
  - Review subagents run as `spawn` children on the deployment default preset rather than inheriting the parent agent's composition (`fork`): a fork child is always promoted by the Anchored Standard bootstrap and its narrowed resident catalog would drop the plain `skill` tool from the review allow-list.
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
+ - 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
+ - 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.
27
29
 
28
30
  ## Configuration
29
31
 
package/lib/index.js CHANGED
@@ -9,10 +9,10 @@ import { validateEvolutionPlan } from "@lmzhen/dsh-evolution-plan-validator";
9
9
  * @module @lmzhen/dsh-evolution-review
10
10
  */
11
11
  const name = "evolution-review";
12
- const inject = ["agents", "tools"];
12
+ const inject = ["agents"];
13
13
  const Config = z.object({
14
14
  reviewEnabled: z.boolean().default(true),
15
- reviewMode: z.string().default("subagent"),
15
+ reviewMode: z.union([z.const("subagent"), z.const("inject")]).default("subagent"),
16
16
  memoryInterval: z.number().default(DEFAULT_REVIEW_MEMORY_INTERVAL),
17
17
  skillInterval: z.number().default(DEFAULT_REVIEW_SKILL_INTERVAL),
18
18
  reviewToolAllow: z.array(z.string()).default(["skill"]),
@@ -31,6 +31,7 @@ function apply(ctx, rawConfig) {
31
31
  const turnStarts = /* @__PURE__ */ new Map();
32
32
  const cumulativeToolCalls = /* @__PURE__ */ new Map();
33
33
  const completionInjected = /* @__PURE__ */ new Set();
34
+ let reviewInFlight = false;
34
35
  const policy = () => ctx.get("evolutionPolicy")?.get();
35
36
  ctx.on("session/event", (session, event) => {
36
37
  if (event.type === "turn/start") turnStarts.set(session.id, session.seq - 1);
@@ -44,6 +45,14 @@ function apply(ctx, rawConfig) {
44
45
  onTurnEnd(session, event);
45
46
  });
46
47
  async function onTurnEnd(session, event) {
48
+ try {
49
+ await runOnTurnEnd(session, event);
50
+ } catch (error) {
51
+ ctx.logger.warn(`dsh-evolution-review: turn-end review pipeline failed: ${error instanceof Error ? error.message : String(error)}`);
52
+ ctx.emit("evolution/review-error", { sessionId: session.id });
53
+ }
54
+ }
55
+ async function runOnTurnEnd(session, event) {
47
56
  if (!config.reviewEnabled) return;
48
57
  if (session.header.origin === "subagent") return;
49
58
  const agent = ctx.agents.get(session.id);
@@ -68,14 +77,14 @@ function apply(ctx, rawConfig) {
68
77
  const cumulative = (cumulativeToolCalls.get(session.id) ?? 0) + signal.toolCalls;
69
78
  cumulativeToolCalls.set(session.id, cumulative);
70
79
  if (kind) {
71
- ctx.emit("evolution/review-scheduled", {
80
+ if (await trySubagentReview(session, agent, kind, signal)) ctx.emit("evolution/review-scheduled", {
72
81
  sessionId: session.id,
73
82
  kind,
74
83
  toolCalls: signal.toolCalls,
75
84
  userChars: signal.userChars,
76
85
  assistantChars: signal.assistantChars
77
86
  });
78
- if (!await trySubagentReview(session, agent, kind, signal)) agent.inject(createUserMessage({
87
+ else agent.inject(createUserMessage({
79
88
  content: [{
80
89
  type: "text",
81
90
  text: reviewPrompt(kind)
@@ -118,9 +127,11 @@ function apply(ctx, rawConfig) {
118
127
  if ((policy()?.reviewMode ?? config.reviewMode) === "inject") return false;
119
128
  const subagents = ctx.get("subagents");
120
129
  if (!subagents) return false;
130
+ if (reviewInFlight) return false;
131
+ reviewInFlight = true;
121
132
  try {
122
- const routingPolicy = ctx.get("evolutionPolicy");
123
- const model = kind === "memory" ? routingPolicy?.get().memoryReviewModel ?? "deepseek-v4-flash" : routingPolicy?.get().skillReviewModel ?? "deepseek-v4-pro";
133
+ const snapshot = policy();
134
+ const model = kind === "memory" ? snapshot?.memoryReviewModel ?? "deepseek-v4-flash" : snapshot?.skillReviewModel ?? "deepseek-v4-pro";
124
135
  const reviewText = redactSecrets(buildReviewRequest(session, kind, signal, config.reviewContextMessages, config.reviewMessageChars));
125
136
  const agentOptions = { model };
126
137
  if (config.reviewProvider) agentOptions.provider = config.reviewProvider;
@@ -154,9 +165,12 @@ function apply(ctx, rawConfig) {
154
165
  });
155
166
  try {
156
167
  const result = await run.result;
157
- if (!result.structured) return true;
168
+ if (!result.structured) {
169
+ ctx.emit("evolution/review-error", { sessionId: session.id });
170
+ ctx.logger.warn("dsh-evolution-review: review subagent returned no structured plan");
171
+ return false;
172
+ }
158
173
  const childReads = run.localAgent ? collectReadSkillNames(run.localAgent.session) : /* @__PURE__ */ new Set();
159
- const snapshot = policy();
160
174
  const plan = result.structured;
161
175
  const policyFingerprint = fingerprintPolicy(snapshot);
162
176
  const validation = validateEvolutionPlan(plan, {
@@ -169,7 +183,8 @@ function apply(ctx, rawConfig) {
169
183
  });
170
184
  const acceptedSkillOps = validation.accepted.skillOps ?? [];
171
185
  const skippedUnread = filterUnreadSkillOps(acceptedSkillOps, new Set([...collectReadSkillNames(session), ...childReads]));
172
- const actions = await executePlan(validation.accepted, session.id);
186
+ const executed = await executePlan(validation.accepted, session.id);
187
+ const actions = executed.actions;
173
188
  const evidenceQuotes = [...validation.accepted.memoryOps ?? [], ...acceptedSkillOps].reduce((total, op) => total + (Array.isArray(op.evidence) ? op.evidence.length : 0), 0);
174
189
  ctx.emit("evolution/plan-applied", {
175
190
  sessionId: session.id,
@@ -181,18 +196,22 @@ function apply(ctx, rawConfig) {
181
196
  evidenceQuotes,
182
197
  estimatedInputChars: reviewText.length
183
198
  });
184
- if (actions.length > 0) agent.inject(createUserMessage({
185
- content: [{
186
- type: "text",
187
- text: `💾 Self-improvement review: ${actions.join(" · ")}`
188
- }],
189
- source: {
190
- kind: "plugin",
191
- plugin: "dsh-evolution-review",
192
- form: "notice",
193
- summary: "self-improvement review"
194
- }
195
- }));
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
+ }
196
215
  return true;
197
216
  } finally {
198
217
  try {
@@ -204,6 +223,8 @@ function apply(ctx, rawConfig) {
204
223
  } catch (error) {
205
224
  ctx.logger.warn(`dsh-evolution-review: subagent review failed: ${error instanceof Error ? error.message : String(error)}`);
206
225
  return false;
226
+ } finally {
227
+ reviewInFlight = false;
207
228
  }
208
229
  }
209
230
  async function executePlan(plan, sessionId) {
@@ -211,6 +232,7 @@ function apply(ctx, rawConfig) {
211
232
  const approval = ctx.get("evolutionApproval");
212
233
  const origins = resolveOrigins(void 0, true);
213
234
  const actions = [];
235
+ let ok = true;
214
236
  for (const op of plan.memoryOps ?? []) {
215
237
  if (!Array.isArray(op.evidence) || op.evidence.length === 0) continue;
216
238
  const normalized = {
@@ -220,6 +242,7 @@ function apply(ctx, rawConfig) {
220
242
  old_text: op.old_text
221
243
  };
222
244
  if ((approval ? await runApproved("memory", `memory ${normalized.target} ${normalized.action}`, normalized, normalized) : await memory?.applyBatch(normalized.target, [normalized]))?.ok) actions.push("Memory updated");
245
+ else ok = false;
223
246
  }
224
247
  for (const op of plan.skillOps ?? []) {
225
248
  if (!Array.isArray(op.evidence) || op.evidence.length === 0 || !op.name) continue;
@@ -232,8 +255,12 @@ function apply(ctx, rawConfig) {
232
255
  origin: origins.library
233
256
  };
234
257
  if ((approval ? await runApproved("skill", `skill ${op.action ?? "patch"} ${op.name}`, runnerArgs, runnerArgs) : await executeSkillDirect(args))?.ok) actions.push(`Skill ${op.name} ${op.action ?? "patch"}`);
258
+ else ok = false;
235
259
  }
236
- return actions;
260
+ return {
261
+ actions,
262
+ ok
263
+ };
237
264
  async function runApproved(kind, summary, stored, runnerArgs) {
238
265
  if (!approval) return void 0;
239
266
  if (approval.isEnabled === true && !approval.hasRunner(kind)) {
@@ -334,12 +361,16 @@ function apply(ctx, rawConfig) {
334
361
  function shouldCompletionReview(reason, sessionToolCalls, minToolCalls) {
335
362
  return reason?.kind === "completed" && sessionToolCalls >= minToolCalls;
336
363
  }
337
- /** Skill names this session loaded (read-before-write source for the background review). */
364
+ /** Skill names this session loaded (read-before-write source for the background review).
365
+ * Only the real `skill` tool is a read (E-59e): the platform has no `skill_load`/
366
+ * `skill_search` discovery pair, so that branch was dead. `skill_manage` has no
367
+ * per-skill read action (its `list`/`review` are whole-library), so a specific
368
+ * skill read through it cannot be tracked — see README Known Limitations. */
338
369
  function collectReadSkillNames(session) {
339
370
  const names = /* @__PURE__ */ new Set();
340
371
  for (const event of session.events) {
341
372
  if (event.type !== "tool/call") continue;
342
- if (event.data.name !== "skill" && event.data.name !== "skill_load") continue;
373
+ if (event.data.name !== "skill") continue;
343
374
  const raw = event.data.arguments;
344
375
  let parsed = {};
345
376
  if (typeof raw === "string") try {
@@ -8,7 +8,7 @@ export declare const name = "evolution-review";
8
8
  export declare const inject: string[];
9
9
  export interface Config {
10
10
  reviewEnabled?: boolean;
11
- reviewMode?: string;
11
+ reviewMode?: 'subagent' | 'inject';
12
12
  memoryInterval?: number;
13
13
  skillInterval?: number;
14
14
  /**
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.17",
4
+ "version": "0.3.19",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -33,8 +33,8 @@
33
33
  "license": "MIT",
34
34
  "dependencies": {
35
35
  "@deepseek-ai/schemastery": "^3.18.1",
36
- "@lmzhen/dsh-evolution-core": "^0.3.17",
37
- "@lmzhen/dsh-evolution-plan-validator": "^0.3.17"
36
+ "@lmzhen/dsh-evolution-core": "^0.3.19",
37
+ "@lmzhen/dsh-evolution-plan-validator": "^0.3.19"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "@deepseek-ai/cordis": "^4.0.1",
@@ -43,7 +43,8 @@
43
43
  "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
44
44
  "@deepseek-ai/dsh-session": "^0.1.1-rc.2",
45
45
  "@deepseek-ai/dsh-tools": "^0.1.1-rc.2",
46
- "@lmzhen/dsh-evolution-state": "^0.3.17"
46
+ "@lmzhen/dsh-evolution-state": "^0.3.19",
47
+ "@lmzhen/dsh-evolution-policy": "^0.3.19"
47
48
  },
48
49
  "devDependencies": {
49
50
  "@deepseek-ai/dsh-agent": "^0.1.1-rc.2",
@@ -54,9 +55,9 @@
54
55
  "@deepseek-ai/dsh-session-persistence": "^0.1.1-rc.2",
55
56
  "@deepseek-ai/dsh-session-persistence-jsonl": "^0.1.1-rc.2",
56
57
  "@deepseek-ai/dsh-tools": "^0.1.1-rc.2",
57
- "@lmzhen/dsh-evolution-approval": "^0.3.17",
58
- "@lmzhen/dsh-evolution-core": "^0.3.17",
59
- "@lmzhen/dsh-evolution-plan-validator": "^0.3.17",
60
- "@lmzhen/dsh-evolution-state": "^0.3.17"
58
+ "@lmzhen/dsh-evolution-approval": "^0.3.19",
59
+ "@lmzhen/dsh-evolution-core": "^0.3.19",
60
+ "@lmzhen/dsh-evolution-plan-validator": "^0.3.19",
61
+ "@lmzhen/dsh-evolution-state": "^0.3.19"
61
62
  }
62
63
  }