@lmzhen/dsh-evolution-review 0.3.66 → 0.3.68

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
@@ -25,7 +25,7 @@ Independent of request-prefix construction. This package does not alter the asse
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
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.
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 — bounded by `REVIEW_STATE_SESSION_CAP` (500, seam constant): the least-recently-active sessions are pruned on save, so a very old session restarting resumes from a fresh cadence baseline rather than an unbounded store.
29
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
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.
31
31
  - Read-before-write can see the review subagent's own `skill` reads only when the subagent backend exposes `localAgent` (the in-process driver does; out-of-process backends such as ACP and the CLI providers set `localAgent: undefined`). With a remote backend the subagent's reads are invisible, so a plan item patching a skill the subagent itself loaded is dropped as "unread" — the review then falls back to the parent session's reads only. Documented rather than worked around: recovering the child read set needs a `SubagentLike` contract change (v14 P2-6).
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_CONTEXT_MESSAGES, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_MESSAGE_CHARS, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_REVIEW_TIMEOUT_MS, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_USER_CHAR_LIMIT, PROMPT_BUNDLE, SkillLibrary, advanceReview, clampedNumber, contentHash, evolutionIoAdapter, foldTurn, redactSecrets, resolveOrigins, resolveRootConfig, resolveSkillsRoot, reviewPrompt, verifyPromptBundle } from "@lmzhen/dsh-evolution-core";
4
+ import { COMPLETION_SKILL_REVIEW_PROMPT, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MEMORY_REVIEW_MODEL, DEFAULT_REVIEW_CONTEXT_MESSAGES, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_MESSAGE_CHARS, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_REVIEW_TIMEOUT_MS, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_MODEL, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_USER_CHAR_LIMIT, MAX_TIMER_DELAY_MS, PROMPT_BUNDLE, SkillLibrary, advanceReview, assertSkillsRootAliasRetired, clampedNumber, contentHash, evolutionIoAdapter, foldTurn, redactSecrets, resolveOrigins, resolveRootConfig, resolveSkillsRoot, 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
  /**
@@ -10,13 +10,6 @@ import { validateEvolutionPlan } from "@lmzhen/dsh-evolution-plan-validator";
10
10
  */
11
11
  const name = "evolution-review";
12
12
  const inject = ["agents"];
13
- /** Node's 32-bit timer-delay ceiling (`AbortSignal.timeout`/`setTimeout`):
14
- * a larger value throws RangeError. B-2 (v18): without a max, a misconfigured
15
- * `reviewTimeoutMs` made `AbortSignal.timeout` throw inside the subagent
16
- * start call; the outer catch logged it and silently degraded the review to
17
- * the inject path. The schema and the assembly clamp both reject it (same
18
- * bound as commands/maintenance). */
19
- const MAX_TIMER_DELAY_MS = 2147483647;
20
13
  const Config = z.object({
21
14
  reviewEnabled: z.boolean().default(true),
22
15
  reviewMode: z.union([z.const("subagent"), z.const("inject")]).default("subagent"),
@@ -81,8 +74,8 @@ function clampReviewConfig(rawConfig, ctx) {
81
74
  function apply(ctx, rawConfig = {}) {
82
75
  if (!verifyPromptBundle(PROMPT_BUNDLE)) throw new Error("dsh-evolution prompt bundle integrity check failed; refusing to schedule review work");
83
76
  const config = clampReviewConfig(rawConfig, ctx);
77
+ assertSkillsRootAliasRetired(rawConfig);
84
78
  const rootConfig = resolveRootConfig(rawConfig);
85
- if (rootConfig.usedDeprecatedAlias) ctx.logger.warn("evolution-review: config \"skillsRoot\" is deprecated (E-7); use \"root\" — the alias is honoured until 0.3.65");
86
79
  const turnStarts = /* @__PURE__ */ new Map();
87
80
  let statelessReviewStateWarned = false;
88
81
  const cumulativeToolCalls = /* @__PURE__ */ new Map();
@@ -93,6 +86,16 @@ function apply(ctx, rawConfig = {}) {
93
86
  const cadenceResetWarned = /* @__PURE__ */ new Set();
94
87
  let reviewInFlight = false;
95
88
  const policy = () => ctx.get("evolutionPolicy")?.get();
89
+ const reviewStateLocks = /* @__PURE__ */ new Map();
90
+ async function withReviewStateLock(id, task) {
91
+ const next = (reviewStateLocks.get(id) ?? Promise.resolve()).catch(() => {}).then(task);
92
+ reviewStateLocks.set(id, next);
93
+ try {
94
+ return await next;
95
+ } finally {
96
+ if (reviewStateLocks.get(id) === next) reviewStateLocks.delete(id);
97
+ }
98
+ }
96
99
  ctx.on("session/event", (session, event) => {
97
100
  if (event.type === "turn/start" && session.header.origin !== "subagent") turnStarts.set(session.id, session.seq - 1);
98
101
  if (event.type !== "turn/end") return;
@@ -132,24 +135,32 @@ function apply(ctx, rawConfig = {}) {
132
135
  statelessReviewStateWarned = true;
133
136
  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).");
134
137
  }
135
- const state = await stateService?.loadReviewState(session.id) ?? {
138
+ const snapshot = policy();
139
+ const skipFire = skipNextCadenceFire.get(session.id) ?? false;
140
+ if (skipFire) skipNextCadenceFire.delete(session.id);
141
+ let state = {
136
142
  turnsSinceMemory: 0,
137
143
  turnsSinceSkill: 0,
138
144
  lastTurn: -1
139
145
  };
140
- const snapshot = policy();
141
- const skipFire = skipNextCadenceFire.get(session.id) ?? false;
142
- if (skipFire) skipNextCadenceFire.delete(session.id);
143
- const rawKind = advanceReview(state, event.data.turn, signal, {
144
- memoryInterval: snapshot?.reviewMemoryInterval ?? config.memoryInterval,
145
- skillInterval: snapshot?.reviewSkillInterval ?? config.skillInterval,
146
- substantiveMinToolCalls: snapshot?.substantiveMinToolCalls ?? 3,
147
- substantiveMinUserChars: snapshot?.substantiveMinUserChars ?? 200,
148
- substantiveMinAgentChars: snapshot?.substantiveMinAgentChars ?? 500,
149
- resetOnFire: false
146
+ const advanced = { kind: null };
147
+ await withReviewStateLock(session.id, async () => {
148
+ state = await stateService?.loadReviewState(session.id) ?? {
149
+ turnsSinceMemory: 0,
150
+ turnsSinceSkill: 0,
151
+ lastTurn: -1
152
+ };
153
+ advanced.kind = advanceReview(state, event.data.turn, signal, {
154
+ memoryInterval: snapshot?.reviewMemoryInterval ?? config.memoryInterval,
155
+ skillInterval: snapshot?.reviewSkillInterval ?? config.skillInterval,
156
+ substantiveMinToolCalls: snapshot?.substantiveMinToolCalls ?? DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS,
157
+ substantiveMinUserChars: snapshot?.substantiveMinUserChars ?? DEFAULT_SUBSTANTIVE_MIN_USER_CHARS,
158
+ substantiveMinAgentChars: snapshot?.substantiveMinAgentChars ?? DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS,
159
+ resetOnFire: false
160
+ });
161
+ await stateService?.saveReviewState(session.id, state);
150
162
  });
151
- const kind = skipFire ? null : rawKind;
152
- await stateService?.saveReviewState(session.id, state);
163
+ const kind = skipFire ? null : advanced.kind;
153
164
  const cumulative = (cumulativeToolCalls.get(session.id) ?? 0) + signal.toolCalls;
154
165
  cumulativeToolCalls.set(session.id, cumulative);
155
166
  if (event.data.reason.kind === "completed") {
@@ -158,6 +169,18 @@ function apply(ctx, rawConfig = {}) {
158
169
  pendingCadenceReviews.delete(session.id);
159
170
  if ((policy()?.reviewMode ?? config.reviewMode) === "inject") try {
160
171
  deliverMessage(agent, reviewPrompt(pendingKind), "auto-review");
172
+ try {
173
+ ctx.emit("evolution/review-scheduled", {
174
+ sessionId: session.id,
175
+ kind: pendingKind,
176
+ toolCalls: signal.toolCalls,
177
+ userChars: signal.userChars,
178
+ assistantChars: signal.assistantChars,
179
+ channel: "inject"
180
+ });
181
+ } catch (emitError) {
182
+ ctx.logger.warn(`dsh-evolution-review: review-scheduled emit failed: ${emitError instanceof Error ? emitError.message : String(emitError)}`);
183
+ }
161
184
  } catch (injectError) {
162
185
  ctx.logger.warn(`dsh-evolution-review: deferred review inject failed: ${injectError instanceof Error ? injectError.message : String(injectError)}`);
163
186
  }
@@ -169,21 +192,40 @@ function apply(ctx, rawConfig = {}) {
169
192
  kind: pendingKind,
170
193
  toolCalls: signal.toolCalls,
171
194
  userChars: signal.userChars,
172
- assistantChars: signal.assistantChars
195
+ assistantChars: signal.assistantChars,
196
+ channel: "subagent"
173
197
  });
174
198
  } catch (emitError) {
175
199
  ctx.logger.warn(`dsh-evolution-review: review-scheduled emit failed: ${emitError instanceof Error ? emitError.message : String(emitError)}`);
176
200
  }
177
- else if (reviewOutcome !== "deferred") try {
201
+ else if (reviewOutcome === "dropped") {
202
+ pendingCadenceReviews.set(session.id, pendingKind);
203
+ return;
204
+ } else if (reviewOutcome !== "deferred") try {
178
205
  deliverMessage(agent, reviewPrompt(pendingKind), "auto-review");
206
+ try {
207
+ ctx.emit("evolution/review-scheduled", {
208
+ sessionId: session.id,
209
+ kind: pendingKind,
210
+ toolCalls: signal.toolCalls,
211
+ userChars: signal.userChars,
212
+ assistantChars: signal.assistantChars,
213
+ channel: "inject"
214
+ });
215
+ } catch (emitError) {
216
+ ctx.logger.warn(`dsh-evolution-review: review-scheduled emit failed: ${emitError instanceof Error ? emitError.message : String(emitError)}`);
217
+ }
179
218
  } catch (injectError) {
180
219
  ctx.logger.warn(`dsh-evolution-review: deferred review inject failed: ${injectError instanceof Error ? injectError.message : String(injectError)}`);
181
220
  }
182
221
  }
183
- state.turnsSinceMemory = 0;
184
- state.turnsSinceSkill = 0;
185
222
  try {
186
- await stateService?.saveReviewState(session.id, state);
223
+ await withReviewStateLock(session.id, async () => {
224
+ const fresh = await stateService?.loadReviewState(session.id) ?? state;
225
+ fresh.turnsSinceMemory = 0;
226
+ fresh.turnsSinceSkill = 0;
227
+ await stateService?.saveReviewState(session.id, fresh);
228
+ });
187
229
  } catch (resetError) {
188
230
  if (!cadenceResetWarned.has(session.id)) {
189
231
  cadenceResetWarned.add(session.id);
@@ -207,6 +249,22 @@ function apply(ctx, rawConfig = {}) {
207
249
  if (completionInjected.has(session.id)) return;
208
250
  if (!shouldCompletionReview(event.data.reason, cumulative, config.skillReviewCompletionMinToolCalls)) return;
209
251
  completionInjected.add(session.id);
252
+ if (reviewInFlight) {
253
+ if (deferredFallbackReviews.length < DEFERRED_REVIEW_CAP) deferredFallbackReviews.push({
254
+ agent,
255
+ sessionId: session.id,
256
+ kind: "skill",
257
+ prompt: COMPLETION_SKILL_REVIEW_PROMPT,
258
+ label: "completion review",
259
+ channel: "completion",
260
+ counts: signal
261
+ });
262
+ else {
263
+ completionInjected.delete(session.id);
264
+ ctx.logger.warn(`dsh-evolution-review: deferred-review queue at cap (${DEFERRED_REVIEW_CAP}) — dropped one deferred completion review prompt; the completion gate re-arms for the next completed turn`);
265
+ }
266
+ return;
267
+ }
210
268
  try {
211
269
  deliverMessage(agent, COMPLETION_SKILL_REVIEW_PROMPT, "completion review");
212
270
  } catch (injectError) {
@@ -220,7 +278,8 @@ function apply(ctx, rawConfig = {}) {
220
278
  kind: "skill",
221
279
  toolCalls: signal.toolCalls,
222
280
  userChars: signal.userChars,
223
- assistantChars: signal.assistantChars
281
+ assistantChars: signal.assistantChars,
282
+ channel: "completion"
224
283
  });
225
284
  } catch (emitError) {
226
285
  ctx.logger.warn(`dsh-evolution-review: review-scheduled emit failed: ${emitError instanceof Error ? emitError.message : String(emitError)}`);
@@ -271,17 +330,25 @@ function apply(ctx, rawConfig = {}) {
271
330
  const subagents = ctx.get("subagents");
272
331
  if (!subagents) return false;
273
332
  if (reviewInFlight) {
274
- if (deferredFallbackReviews.length < DEFERRED_REVIEW_CAP) deferredFallbackReviews.push({
275
- agent,
276
- kind
277
- });
278
- else ctx.logger.warn(`dsh-evolution-review: deferred-review queue at cap (${DEFERRED_REVIEW_CAP}) — dropping one fallback review prompt`);
279
- return "deferred";
333
+ if (deferredFallbackReviews.length < DEFERRED_REVIEW_CAP) {
334
+ deferredFallbackReviews.push({
335
+ agent,
336
+ sessionId: session.id,
337
+ kind,
338
+ prompt: reviewPrompt(kind),
339
+ label: "auto-review",
340
+ channel: "inject",
341
+ counts: signal
342
+ });
343
+ return "deferred";
344
+ }
345
+ ctx.logger.warn(`dsh-evolution-review: deferred-review queue at cap (${DEFERRED_REVIEW_CAP}) — dropping one fallback review prompt`);
346
+ return "dropped";
280
347
  }
281
348
  reviewInFlight = true;
282
349
  try {
283
350
  const snapshot = policy();
284
- const model = kind === "memory" ? snapshot?.memoryReviewModel ?? "deepseek-v4-flash" : snapshot?.skillReviewModel ?? "deepseek-v4-pro";
351
+ const model = kind === "memory" ? snapshot?.memoryReviewModel ?? DEFAULT_MEMORY_REVIEW_MODEL : snapshot?.skillReviewModel ?? DEFAULT_SKILL_REVIEW_MODEL;
285
352
  const reviewText = redactSecrets(buildReviewRequest(session, kind, signal, config.reviewContextMessages, config.reviewMessageChars));
286
353
  const agentOptions = { model };
287
354
  if (config.reviewProvider) agentOptions.provider = config.reviewProvider;
@@ -323,9 +390,38 @@ function apply(ctx, rawConfig = {}) {
323
390
  });
324
391
  const acceptedSkillOps = validation.accepted.skillOps ?? [];
325
392
  const skippedUnread = filterUnreadSkillOps(acceptedSkillOps, new Set([...collectReadSkillNames(session), ...childReads]));
326
- const executed = await withTimeout(executePlan(validation.accepted, session), config.reviewTimeoutMs, "review plan execution");
327
- const actions = executed.actions;
328
393
  const evidenceQuotes = [...validation.accepted.memoryOps ?? [], ...acceptedSkillOps].reduce((total, op) => total + (Array.isArray(op.evidence) ? op.evidence.length : 0), 0);
394
+ const emitApplied = (report) => {
395
+ try {
396
+ ctx.emit("evolution/plan-applied", {
397
+ sessionId: session.id,
398
+ planId: randomUUID(),
399
+ policyFingerprint,
400
+ memoryApplied: report.actions.filter((action) => action.startsWith("Memory")).length,
401
+ skillApplied: report.actions.filter((action) => action.startsWith("Skill ")).length,
402
+ rejectedOps: validation.rejected.length,
403
+ ...skippedUnread > 0 ? { skippedUnread } : {},
404
+ executionFailures: report.failedOps?.length ?? 0,
405
+ ...report.executionError !== void 0 ? { executionError: report.executionError } : {},
406
+ evidenceQuotes,
407
+ estimatedInputChars: reviewText.length
408
+ });
409
+ } catch (emitError) {
410
+ ctx.logger.warn(`dsh-evolution-review: plan-applied emit failed: ${emitError instanceof Error ? emitError.message : String(emitError)}`);
411
+ }
412
+ };
413
+ const landed = [];
414
+ let executed;
415
+ try {
416
+ executed = await withTimeout(executePlan(validation.accepted, session, (action) => landed.push(action)), config.reviewTimeoutMs, "review plan execution");
417
+ } catch (error) {
418
+ emitApplied({
419
+ actions: landed,
420
+ executionError: `execution timed out after ${config.reviewTimeoutMs}ms`
421
+ });
422
+ throw error;
423
+ }
424
+ const actions = executed.actions;
329
425
  if (actions.length > 0) {
330
426
  const applied = actions.join(" · ");
331
427
  const note = executed.ok ? "" : "\n部分操作失败。以下操作已应用,请勿重复执行。";
@@ -347,23 +443,11 @@ function apply(ctx, rawConfig = {}) {
347
443
  } catch (injectError) {
348
444
  ctx.logger.warn(`dsh-evolution-review: zero-landing notice inject failed: ${injectError instanceof Error ? injectError.message : String(injectError)}`);
349
445
  }
350
- try {
351
- ctx.emit("evolution/plan-applied", {
352
- sessionId: session.id,
353
- planId: randomUUID(),
354
- policyFingerprint,
355
- memoryApplied: actions.filter((action) => action.startsWith("Memory")).length,
356
- skillApplied: actions.filter((action) => action.startsWith("Skill ")).length,
357
- rejectedOps: validation.rejected.length + skippedUnread,
358
- executionFailures: executed.failedOps.length,
359
- ...executed.aborted !== void 0 ? { executionError: executed.aborted } : {},
360
- ...executed.failedOps[0] !== void 0 && executed.aborted === void 0 ? { executionError: executed.failedOps[0] } : {},
361
- evidenceQuotes,
362
- estimatedInputChars: reviewText.length
363
- });
364
- } catch (emitError) {
365
- ctx.logger.warn(`dsh-evolution-review: plan-applied emit failed: ${emitError instanceof Error ? emitError.message : String(emitError)}`);
366
- }
446
+ emitApplied({
447
+ actions,
448
+ failedOps: executed.failedOps,
449
+ ...executed.aborted !== void 0 ? { executionError: executed.aborted } : executed.failedOps[0] !== void 0 ? { executionError: executed.failedOps[0] } : {}
450
+ });
367
451
  return true;
368
452
  } finally {
369
453
  try {
@@ -379,14 +463,30 @@ function apply(ctx, rawConfig = {}) {
379
463
  } finally {
380
464
  reviewInFlight = false;
381
465
  const deferred = deferredFallbackReviews.splice(0);
382
- for (const { agent: waitingAgent, kind: waitingKind } of deferred) try {
383
- deliverMessage(waitingAgent, reviewPrompt(waitingKind), "auto-review");
384
- } catch (injectError) {
385
- ctx.logger.warn(`dsh-evolution-review: deferred review inject failed: ${injectError instanceof Error ? injectError.message : String(injectError)}`);
466
+ for (const { agent: waitingAgent, sessionId: entrySession, kind: waitingKind, prompt, label, channel, counts: entryCounts } of deferred) {
467
+ try {
468
+ deliverMessage(waitingAgent, prompt, label);
469
+ } catch (injectError) {
470
+ if (channel === "completion") completionInjected.delete(entrySession);
471
+ ctx.logger.warn(`dsh-evolution-review: deferred review inject failed: ${injectError instanceof Error ? injectError.message : String(injectError)}`);
472
+ continue;
473
+ }
474
+ try {
475
+ ctx.emit("evolution/review-scheduled", {
476
+ sessionId: entrySession,
477
+ kind: waitingKind,
478
+ toolCalls: entryCounts.toolCalls,
479
+ userChars: entryCounts.userChars,
480
+ assistantChars: entryCounts.assistantChars,
481
+ channel
482
+ });
483
+ } catch (emitError) {
484
+ ctx.logger.warn(`dsh-evolution-review: review-scheduled emit failed: ${emitError instanceof Error ? emitError.message : String(emitError)}`);
485
+ }
386
486
  }
387
487
  }
388
488
  }
389
- async function executePlan(plan, session) {
489
+ async function executePlan(plan, session, onLanded) {
390
490
  const sessionId = session?.id;
391
491
  const memory = ctx.get("memory");
392
492
  const approval = ctx.get("evolutionApproval");
@@ -413,8 +513,10 @@ function apply(ctx, rawConfig = {}) {
413
513
  old_text: op.old_text
414
514
  };
415
515
  const result = approval ? await runApproved("memory", `memory ${normalized.target} ${normalized.action}`, normalized, normalized, session) : await memory?.applyBatch(normalized.target, [normalized]);
416
- if (result?.ok) actions.push("Memory updated");
417
- else {
516
+ if (result?.ok) {
517
+ actions.push("Memory updated");
518
+ onLanded?.("Memory updated");
519
+ } else {
418
520
  ok = false;
419
521
  failedOps.push(`memory ${normalized.action} ${normalized.target}: ${result?.message ?? "service unavailable"}`);
420
522
  }
@@ -438,8 +540,10 @@ function apply(ctx, rawConfig = {}) {
438
540
  origin: origins.library
439
541
  };
440
542
  const result = approval ? await runApproved("skill", `skill ${op.action ?? "patch"} ${op.name}`, runnerArgs, runnerArgs, session) : await executeSkillDirect(args);
441
- if (result?.ok) actions.push(`Skill ${op.name} ${op.action ?? "patch"}`);
442
- else {
543
+ if (result?.ok) {
544
+ actions.push(`Skill ${op.name} ${op.action ?? "patch"}`);
545
+ onLanded?.(`Skill ${op.name} ${op.action ?? "patch"}`);
546
+ } else {
443
547
  ok = false;
444
548
  failedOps.push(`skill ${op.action ?? "patch"} ${op.name}: ${result?.message ?? "service unavailable"}`);
445
549
  }
@@ -559,6 +663,7 @@ function apply(ctx, rawConfig = {}) {
559
663
  pendingCadenceWarned.clear();
560
664
  skipNextCadenceFire.clear();
561
665
  cadenceResetWarned.clear();
666
+ reviewStateLocks.clear();
562
667
  }, "dsh-evolution-review.cleanup");
563
668
  }
564
669
  /** Completion-channel decision: task finished normally AND the session is proven long. */
@@ -47,8 +47,9 @@ export interface Config {
47
47
  * skills in the SAME tree the catalog/tools read instead of writing a
48
48
  * parallel tree the rest of the family cannot see. E-7 (v18): canonical key. */
49
49
  root?: string;
50
- /** Deprecated alias of `root` (E-7, v18); honoured only while `root` is
51
- * empty, with a warning; removed after 0.3.65. */
50
+ /** V27 G2.4 (M-08): the retired `skillsRoot` alias, kept in the schema so a
51
+ * config that still sets it reaches {@link assertSkillsRootAliasRetired} and
52
+ * fails the load instead of being silently dropped. Never read. */
52
53
  skillsRoot?: string;
53
54
  }
54
55
  export declare const Config: z<Config>;
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.66",
4
+ "version": "0.3.68",
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.66",
35
- "@lmzhen/dsh-evolution-core": "^0.3.66",
36
- "@lmzhen/dsh-evolution-plan-validator": "^0.3.66"
34
+ "@lmzhen/dsh-evolution-approval": "^0.3.68",
35
+ "@lmzhen/dsh-evolution-core": "^0.3.68",
36
+ "@lmzhen/dsh-evolution-plan-validator": "^0.3.68"
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.66",
46
- "@lmzhen/dsh-evolution-policy": "^0.3.66"
45
+ "@lmzhen/dsh-evolution-state": "^0.3.68",
46
+ "@lmzhen/dsh-evolution-policy": "^0.3.68"
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.66",
58
- "@lmzhen/dsh-evolution-core": "^0.3.66",
59
- "@lmzhen/dsh-evolution-plan-validator": "^0.3.66",
60
- "@lmzhen/dsh-evolution-state": "^0.3.66"
57
+ "@lmzhen/dsh-evolution-approval": "^0.3.68",
58
+ "@lmzhen/dsh-evolution-core": "^0.3.68",
59
+ "@lmzhen/dsh-evolution-plan-validator": "^0.3.68",
60
+ "@lmzhen/dsh-evolution-state": "^0.3.68"
61
61
  }
62
62
  }