@lmzhen/dsh-evolution-review 0.3.16 → 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/README.md CHANGED
@@ -24,6 +24,8 @@ Independent of request-prefix construction. This package does not alter the asse
24
24
  - Review subagents inherit the host preset; Anchored Standard deployments rely on the default `skill_search`/`skill_load` allow-list.
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);
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,13 +223,16 @@ 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
- async function executePlan(plan) {
230
+ async function executePlan(plan, sessionId) {
210
231
  const memory = ctx.get("memory");
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)) {
@@ -247,14 +274,15 @@ function apply(ctx, rawConfig) {
247
274
  kind,
248
275
  summary,
249
276
  args: stored,
250
- origin: origins.approval
277
+ origin: origins.approval,
278
+ ...sessionId ? { sessionId } : {}
251
279
  });
252
280
  if (decision.action === "staged") return {
253
281
  ok: false,
254
282
  message: decision.message
255
283
  };
256
284
  if (approval.isEnabled === false) return await runnerDirect(kind, runnerArgs);
257
- return await approval.run(kind, runnerArgs);
285
+ return await approval.run(kind, runnerArgs, { interface: "background_review" });
258
286
  }
259
287
  /** Direct execution for the approval-disabled case (parallel to executeSkillDirect). */
260
288
  async function runnerDirect(kind, args) {
@@ -333,12 +361,16 @@ function apply(ctx, rawConfig) {
333
361
  function shouldCompletionReview(reason, sessionToolCalls, minToolCalls) {
334
362
  return reason?.kind === "completed" && sessionToolCalls >= minToolCalls;
335
363
  }
336
- /** 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. */
337
369
  function collectReadSkillNames(session) {
338
370
  const names = /* @__PURE__ */ new Set();
339
371
  for (const event of session.events) {
340
372
  if (event.type !== "tool/call") continue;
341
- if (event.data.name !== "skill" && event.data.name !== "skill_load") continue;
373
+ if (event.data.name !== "skill") continue;
342
374
  const raw = event.data.arguments;
343
375
  let parsed = {};
344
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.16",
4
+ "version": "0.3.18",
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.16",
37
- "@lmzhen/dsh-evolution-plan-validator": "^0.3.16"
36
+ "@lmzhen/dsh-evolution-core": "^0.3.18",
37
+ "@lmzhen/dsh-evolution-plan-validator": "^0.3.18"
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.16"
46
+ "@lmzhen/dsh-evolution-state": "^0.3.18",
47
+ "@lmzhen/dsh-evolution-policy": "^0.3.18"
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.16",
58
- "@lmzhen/dsh-evolution-core": "^0.3.16",
59
- "@lmzhen/dsh-evolution-plan-validator": "^0.3.16",
60
- "@lmzhen/dsh-evolution-state": "^0.3.16"
58
+ "@lmzhen/dsh-evolution-approval": "^0.3.18",
59
+ "@lmzhen/dsh-evolution-core": "^0.3.18",
60
+ "@lmzhen/dsh-evolution-plan-validator": "^0.3.18",
61
+ "@lmzhen/dsh-evolution-state": "^0.3.18"
61
62
  }
62
63
  }