@lmzhen/dsh-evolution-review 0.3.67 → 0.3.69

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,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_LIMITS, 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"),
@@ -78,11 +71,17 @@ function clampReviewConfig(rawConfig, ctx) {
78
71
  if (clamped.length > 0) ctx.logger.warn(`dsh-evolution-review: ${clamped.join(", ")} provided an invalid value; falling back to the default`);
79
72
  return config;
80
73
  }
74
+ /** v30 REV-04/REV-02: read the policy snapshot off the (optional) policy
75
+ * service through an `unknown` boundary — the Context augmentation types the
76
+ * getter non-optionally, but at runtime the row can be absent. */
77
+ function policySnapshotOf(source) {
78
+ return source?.get?.();
79
+ }
81
80
  function apply(ctx, rawConfig = {}) {
82
81
  if (!verifyPromptBundle(PROMPT_BUNDLE)) throw new Error("dsh-evolution prompt bundle integrity check failed; refusing to schedule review work");
83
82
  const config = clampReviewConfig(rawConfig, ctx);
83
+ assertSkillsRootAliasRetired(rawConfig);
84
84
  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
85
  const turnStarts = /* @__PURE__ */ new Map();
87
86
  let statelessReviewStateWarned = false;
88
87
  const cumulativeToolCalls = /* @__PURE__ */ new Map();
@@ -160,9 +159,9 @@ function apply(ctx, rawConfig = {}) {
160
159
  advanced.kind = advanceReview(state, event.data.turn, signal, {
161
160
  memoryInterval: snapshot?.reviewMemoryInterval ?? config.memoryInterval,
162
161
  skillInterval: snapshot?.reviewSkillInterval ?? config.skillInterval,
163
- substantiveMinToolCalls: snapshot?.substantiveMinToolCalls ?? 3,
164
- substantiveMinUserChars: snapshot?.substantiveMinUserChars ?? 200,
165
- substantiveMinAgentChars: snapshot?.substantiveMinAgentChars ?? 500,
162
+ substantiveMinToolCalls: snapshot?.substantiveMinToolCalls ?? DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS,
163
+ substantiveMinUserChars: snapshot?.substantiveMinUserChars ?? DEFAULT_SUBSTANTIVE_MIN_USER_CHARS,
164
+ substantiveMinAgentChars: snapshot?.substantiveMinAgentChars ?? DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS,
166
165
  resetOnFire: false
167
166
  });
168
167
  await stateService?.saveReviewState(session.id, state);
@@ -205,7 +204,10 @@ function apply(ctx, rawConfig = {}) {
205
204
  } catch (emitError) {
206
205
  ctx.logger.warn(`dsh-evolution-review: review-scheduled emit failed: ${emitError instanceof Error ? emitError.message : String(emitError)}`);
207
206
  }
208
- else if (reviewOutcome !== "deferred") try {
207
+ else if (reviewOutcome === "dropped") {
208
+ pendingCadenceReviews.set(session.id, pendingKind);
209
+ return;
210
+ } else if (reviewOutcome !== "deferred") try {
209
211
  deliverMessage(agent, reviewPrompt(pendingKind), "auto-review");
210
212
  try {
211
213
  ctx.emit("evolution/review-scheduled", {
@@ -329,30 +331,54 @@ function apply(ctx, rawConfig = {}) {
329
331
  ctx.effect(() => () => {
330
332
  deferredFallbackReviews.length = 0;
331
333
  }, "dsh-evolution-review.deferred-drain");
334
+ /** v32 REV-06(b): content hash of every tree skill at REVIEW SCHEDULE time.
335
+ * The subagent's read-time hash is not plumbed through the plan, so this
336
+ * pre-run snapshot is what the execute-time staleness check can honestly
337
+ * compare against: a skill whose hash changed while the review ran was
338
+ * touched concurrently, and the plan (authored against the old state) is
339
+ * refused as stale. */
340
+ async function treeSkillHashes() {
341
+ const hashes = /* @__PURE__ */ new Map();
342
+ const io = ctx.get("evolutionIo");
343
+ if (!io) return hashes;
344
+ const library = new SkillLibrary(resolveSkillsRoot({ root: rootConfig.root }), evolutionIoAdapter(() => io.provider()));
345
+ for (const summary of await library.list().catch((error) => {
346
+ ctx.logger.warn(`dsh-evolution-review: skill tree scan failed (${error instanceof Error ? error.message : String(error)}) — pre-run staleness hashes are unavailable; full-content updates will not be drift-checked this review`);
347
+ return [];
348
+ })) {
349
+ const text = await library.read(summary.name).catch(() => null);
350
+ if (text !== null) hashes.set(summary.name, contentHash(text));
351
+ }
352
+ return hashes;
353
+ }
332
354
  async function trySubagentReview(session, agent, kind, signal) {
333
355
  if ((policy()?.reviewMode ?? config.reviewMode) === "inject") return false;
334
356
  const subagents = ctx.get("subagents");
335
357
  if (!subagents) return false;
336
358
  if (reviewInFlight) {
337
- if (deferredFallbackReviews.length < DEFERRED_REVIEW_CAP) deferredFallbackReviews.push({
338
- agent,
339
- sessionId: session.id,
340
- kind,
341
- prompt: reviewPrompt(kind),
342
- label: "auto-review",
343
- channel: "inject",
344
- counts: signal
345
- });
346
- else ctx.logger.warn(`dsh-evolution-review: deferred-review queue at cap (${DEFERRED_REVIEW_CAP}) — dropping one fallback review prompt`);
347
- return "deferred";
359
+ if (deferredFallbackReviews.length < DEFERRED_REVIEW_CAP) {
360
+ deferredFallbackReviews.push({
361
+ agent,
362
+ sessionId: session.id,
363
+ kind,
364
+ prompt: reviewPrompt(kind),
365
+ label: "auto-review",
366
+ channel: "inject",
367
+ counts: signal
368
+ });
369
+ return "deferred";
370
+ }
371
+ ctx.logger.warn(`dsh-evolution-review: deferred-review queue at cap (${DEFERRED_REVIEW_CAP}) — dropping one fallback review prompt`);
372
+ return "dropped";
348
373
  }
349
374
  reviewInFlight = true;
350
375
  try {
351
376
  const snapshot = policy();
352
- const model = kind === "memory" ? snapshot?.memoryReviewModel ?? "deepseek-v4-flash" : snapshot?.skillReviewModel ?? "deepseek-v4-pro";
377
+ const model = kind === "memory" ? snapshot?.memoryReviewModel ?? DEFAULT_MEMORY_REVIEW_MODEL : snapshot?.skillReviewModel ?? DEFAULT_SKILL_REVIEW_MODEL;
353
378
  const reviewText = redactSecrets(buildReviewRequest(session, kind, signal, config.reviewContextMessages, config.reviewMessageChars));
354
379
  const agentOptions = { model };
355
380
  if (config.reviewProvider) agentOptions.provider = config.reviewProvider;
381
+ const preRunHashes = await treeSkillHashes();
356
382
  const run = await subagents.start("spawn", {
357
383
  label: "dsh-evolution-review",
358
384
  prompt: [{
@@ -391,12 +417,42 @@ function apply(ctx, rawConfig = {}) {
391
417
  });
392
418
  const acceptedSkillOps = validation.accepted.skillOps ?? [];
393
419
  const skippedUnread = filterUnreadSkillOps(acceptedSkillOps, new Set([...collectReadSkillNames(session), ...childReads]));
394
- const executed = await withTimeout(executePlan(validation.accepted, session), config.reviewTimeoutMs, "review plan execution");
395
- const actions = executed.actions;
396
420
  const evidenceQuotes = [...validation.accepted.memoryOps ?? [], ...acceptedSkillOps].reduce((total, op) => total + (Array.isArray(op.evidence) ? op.evidence.length : 0), 0);
421
+ const emitApplied = (report) => {
422
+ try {
423
+ ctx.emit("evolution/plan-applied", {
424
+ sessionId: session.id,
425
+ planId: randomUUID(),
426
+ policyFingerprint,
427
+ memoryApplied: report.actions.filter((action) => action.startsWith("Memory")).length,
428
+ skillApplied: report.actions.filter((action) => action.startsWith("Skill ")).length,
429
+ rejectedOps: validation.rejected.length,
430
+ ...skippedUnread > 0 ? { skippedUnread } : {},
431
+ executionFailures: report.failedOps?.length ?? 0,
432
+ ...report.executionError !== void 0 ? { executionError: report.executionError } : {},
433
+ evidenceQuotes,
434
+ estimatedInputChars: reviewText.length
435
+ });
436
+ } catch (emitError) {
437
+ ctx.logger.warn(`dsh-evolution-review: plan-applied emit failed: ${emitError instanceof Error ? emitError.message : String(emitError)}`);
438
+ }
439
+ };
440
+ const landed = [];
441
+ let executed;
442
+ try {
443
+ executed = await withTimeout(executePlan(validation.accepted, session, (action) => landed.push(action), preRunHashes), config.reviewTimeoutMs, "review plan execution");
444
+ } catch (error) {
445
+ emitApplied({
446
+ actions: landed,
447
+ executionError: `execution timed out after ${config.reviewTimeoutMs}ms`
448
+ });
449
+ throw error;
450
+ }
451
+ const actions = executed.actions;
397
452
  if (actions.length > 0) {
398
453
  const applied = actions.join(" · ");
399
- const note = executed.ok ? "" : "\n部分操作失败。以下操作已应用,请勿重复执行。";
454
+ const failedNote = executed.failedOps.length > 0 ? ` 失败 ${executed.failedOps.length} 个:${executed.failedOps.join("")}。` : "";
455
+ const note = executed.ok ? "" : `\n部分操作失败。${failedNote}以上操作已应用,请勿重复执行。`;
400
456
  try {
401
457
  deliverMessage(agent, `💾 Self-improvement review: ${applied}${note}`, "self-improvement review");
402
458
  } catch (injectError) {
@@ -415,23 +471,11 @@ function apply(ctx, rawConfig = {}) {
415
471
  } catch (injectError) {
416
472
  ctx.logger.warn(`dsh-evolution-review: zero-landing notice inject failed: ${injectError instanceof Error ? injectError.message : String(injectError)}`);
417
473
  }
418
- try {
419
- ctx.emit("evolution/plan-applied", {
420
- sessionId: session.id,
421
- planId: randomUUID(),
422
- policyFingerprint,
423
- memoryApplied: actions.filter((action) => action.startsWith("Memory")).length,
424
- skillApplied: actions.filter((action) => action.startsWith("Skill ")).length,
425
- rejectedOps: validation.rejected.length + skippedUnread,
426
- executionFailures: executed.failedOps.length,
427
- ...executed.aborted !== void 0 ? { executionError: executed.aborted } : {},
428
- ...executed.failedOps[0] !== void 0 && executed.aborted === void 0 ? { executionError: executed.failedOps[0] } : {},
429
- evidenceQuotes,
430
- estimatedInputChars: reviewText.length
431
- });
432
- } catch (emitError) {
433
- ctx.logger.warn(`dsh-evolution-review: plan-applied emit failed: ${emitError instanceof Error ? emitError.message : String(emitError)}`);
434
- }
474
+ emitApplied({
475
+ actions,
476
+ failedOps: executed.failedOps,
477
+ ...executed.aborted !== void 0 ? { executionError: executed.aborted } : executed.failedOps[0] !== void 0 ? { executionError: executed.failedOps[0] } : {}
478
+ });
435
479
  return true;
436
480
  } finally {
437
481
  try {
@@ -470,7 +514,7 @@ function apply(ctx, rawConfig = {}) {
470
514
  }
471
515
  }
472
516
  }
473
- async function executePlan(plan, session) {
517
+ async function executePlan(plan, session, onLanded, preRunHashes) {
474
518
  const sessionId = session?.id;
475
519
  const memory = ctx.get("memory");
476
520
  const approval = ctx.get("evolutionApproval");
@@ -497,8 +541,10 @@ function apply(ctx, rawConfig = {}) {
497
541
  old_text: op.old_text
498
542
  };
499
543
  const result = approval ? await runApproved("memory", `memory ${normalized.target} ${normalized.action}`, normalized, normalized, session) : await memory?.applyBatch(normalized.target, [normalized]);
500
- if (result?.ok) actions.push("Memory updated");
501
- else {
544
+ if (result?.ok) {
545
+ actions.push("Memory updated");
546
+ onLanded?.("Memory updated");
547
+ } else {
502
548
  ok = false;
503
549
  failedOps.push(`memory ${normalized.action} ${normalized.target}: ${result?.message ?? "service unavailable"}`);
504
550
  }
@@ -517,13 +563,34 @@ function apply(ctx, rawConfig = {}) {
517
563
  const stageCurrent = await hashLibrary.read(args.name).catch(() => null);
518
564
  if (stageCurrent !== null) args.staged_from_sha256 = contentHash(stageCurrent);
519
565
  }
566
+ if ((args.action === "write_file" || args.action === "remove_file") && hashLibrary && typeof args.name === "string" && args.name !== "" && typeof args.file_path === "string" && args.file_path !== "") {
567
+ const stageFile = await hashLibrary.readSupportFile(args.name, args.file_path).catch(() => void 0);
568
+ if (stageFile !== void 0) args.staged_from_sha256 = stageFile === null ? "absent" : contentHash(stageFile);
569
+ }
570
+ const opName = typeof args.name === "string" ? args.name : "";
571
+ const hashChecked = (args.action === "update" || args.action === "edit") && opName !== "" && preRunHashes?.has(opName) === true;
572
+ if (hashChecked) {
573
+ const live = await hashLibrary?.read(opName).catch(() => null) ?? null;
574
+ const preRun = preRunHashes.get(opName);
575
+ if (live === null || preRun !== void 0 && contentHash(live) !== preRun) {
576
+ ok = false;
577
+ failedOps.push(`skill ${args.action} ${args.name}: the skill changed while this review ran — update refused as stale; produce a fresh plan`);
578
+ continue;
579
+ }
580
+ }
520
581
  const runnerArgs = {
521
582
  operation: args,
522
583
  origin: origins.library
523
584
  };
524
585
  const result = approval ? await runApproved("skill", `skill ${op.action ?? "patch"} ${op.name}`, runnerArgs, runnerArgs, session) : await executeSkillDirect(args);
525
- if (result?.ok) actions.push(`Skill ${op.name} ${op.action ?? "patch"}`);
526
- else {
586
+ if (result?.ok) {
587
+ actions.push(`Skill ${op.name} ${op.action ?? "patch"}`);
588
+ onLanded?.(`Skill ${op.name} ${op.action ?? "patch"}`);
589
+ if (hashChecked) {
590
+ const landed = await hashLibrary?.read(opName).catch(() => null) ?? null;
591
+ if (landed !== null) preRunHashes.set(opName, contentHash(landed));
592
+ }
593
+ } else {
527
594
  ok = false;
528
595
  failedOps.push(`skill ${op.action ?? "patch"} ${op.name}: ${result?.message ?? "service unavailable"}`);
529
596
  }
@@ -597,19 +664,55 @@ function apply(ctx, rawConfig = {}) {
597
664
  ok: false,
598
665
  message: "evolution-io service not mounted"
599
666
  };
600
- const library = new SkillLibrary(resolveSkillsRoot({ root: rootConfig.root }), evolutionIoAdapter(() => io.provider()), void 0, (event) => {
667
+ const policySnapshot = policySnapshotOf(ctx.get("evolutionPolicy"));
668
+ const library = new SkillLibrary(resolveSkillsRoot({ root: rootConfig.root }), evolutionIoAdapter(() => io.provider()), {
669
+ ...DEFAULT_SKILL_LIMITS,
670
+ maxSkillContentChars: policySnapshot?.skillContentChars ?? DEFAULT_SKILL_LIMITS.maxSkillContentChars
671
+ }, (event) => {
601
672
  ctx.emit("evolution/skill-mutated", event);
602
673
  });
603
674
  const op = skillArgs;
604
675
  const name = op.name ?? "";
605
676
  const origin = origins.library;
677
+ const protectedNames = policySnapshot?.protectedSkillNames;
678
+ if (origin !== "foreground" && protectedNames?.includes(name) && op.action !== "create") return {
679
+ ok: false,
680
+ message: `Skill "${name}" is protected by the current policy (protectedSkillNames); autonomous writes are refused.`
681
+ };
682
+ if (op.action === "write_file" || op.action === "remove_file") {
683
+ const expected = op.staged_from_sha256;
684
+ if (typeof expected === "string" && expected !== "") {
685
+ const current = await library.readSupportFile(name, op.file_path ?? "").catch(() => void 0);
686
+ const actual = current === void 0 || current === null ? "absent" : contentHash(current);
687
+ if (current === void 0 || actual !== expected) return {
688
+ ok: false,
689
+ message: `Support file "${op.file_path ?? ""}" of "${name}" changed since this plan was produced — the staged file operation was refused as stale. Re-read the skill tree and produce a fresh plan.`
690
+ };
691
+ }
692
+ }
606
693
  if (op.action === "create") {
607
694
  const created = await library.create(name, op.content ?? "", origin);
608
695
  if (created.ok) await ctx.get("skillUsage")?.markAgentCreated?.(name);
609
696
  return created;
610
697
  }
611
- if (op.action === "edit" || op.action === "update") return await library.update(name, op.content ?? "", origin);
612
- if (op.action === "patch") return await library.patch(name, op.old_string ?? "", op.new_string ?? "", op.file_path ?? "", false, origin);
698
+ if (op.action === "edit" || op.action === "update") {
699
+ const expected = op.staged_from_sha256;
700
+ if (typeof expected === "string" && expected !== "") {
701
+ const current = await library.read(name).catch(() => null);
702
+ if (current !== null && contentHash(current) !== expected) return {
703
+ ok: false,
704
+ message: `Skill "${name}" changed since this plan was produced — the full-content update was refused as stale. Re-read the skill and produce a fresh plan.`
705
+ };
706
+ }
707
+ const updated = await library.update(name, op.content ?? "", origin);
708
+ if (updated.ok) await ctx.get("skillUsage")?.record?.(name, "patch").catch(() => {});
709
+ return updated;
710
+ }
711
+ if (op.action === "patch") {
712
+ const patched = await library.patch(name, op.old_string ?? "", op.new_string ?? "", op.file_path ?? "", false, origin);
713
+ if (patched.ok) await ctx.get("skillUsage")?.record?.(name, "patch").catch(() => {});
714
+ return patched;
715
+ }
613
716
  if (op.action === "delete") {
614
717
  const into = (op.absorbed_into ?? "").trim();
615
718
  if (!into || !await library.read(into)) return {
@@ -620,8 +723,21 @@ function apply(ctx, rawConfig = {}) {
620
723
  if (archived.ok) await ctx.get("skillUsage")?.markArchived?.(name);
621
724
  return archived;
622
725
  }
623
- if (op.action === "write_file") return await library.writeSupportFile(name, op.file_path ?? "", op.file_content ?? "", origin);
624
- if (op.action === "remove_file") return await library.removeSupportFile(name, op.file_path ?? "", origin);
726
+ if (op.action === "write_file" || op.action === "remove_file") {
727
+ const expected = op.staged_from_sha256;
728
+ if (typeof expected === "string" && expected !== "") {
729
+ const current = await library.readSupportFile(name, op.file_path ?? "").catch(() => void 0);
730
+ const actual = current === void 0 || current === null ? "absent" : contentHash(current);
731
+ if (current === void 0 || actual !== expected) return {
732
+ ok: false,
733
+ message: `Support file "${op.file_path ?? ""}" of "${name}" changed since this plan was produced — the staged file operation was refused as stale. Re-read the skill tree and produce a fresh plan.`
734
+ };
735
+ }
736
+ if (op.action === "write_file") return await library.writeSupportFile(name, op.file_path ?? "", op.file_content ?? "", origin);
737
+ const removedSupport = await library.removeSupportFile(name, op.file_path ?? "", origin);
738
+ if (removedSupport.ok) await ctx.get("skillUsage")?.record?.(name, "patch").catch(() => {});
739
+ return removedSupport;
740
+ }
625
741
  if (op.action === "restructure") {
626
742
  const moves = (op.restructure ?? []).filter((move) => move !== null).map((move) => ({
627
743
  heading: move.heading ?? "",
@@ -656,9 +772,9 @@ function shouldCompletionReview(reason, sessionToolCalls, minToolCalls) {
656
772
  * per-skill read action (its `list`/`review` are whole-library), so a specific
657
773
  * skill read through it cannot be tracked — see README Known Limitations. */
658
774
  function collectReadSkillNames(session) {
659
- const names = /* @__PURE__ */ new Set();
660
- for (const event of session.events) {
661
- if (event.type !== "tool/call") continue;
775
+ const callNames = /* @__PURE__ */ new Map();
776
+ const okCallIds = /* @__PURE__ */ new Set();
777
+ for (const event of session.events) if (event.type === "tool/call") {
662
778
  if (event.data.name !== "skill") continue;
663
779
  const raw = event.data.arguments;
664
780
  let parsed = {};
@@ -667,11 +783,20 @@ function collectReadSkillNames(session) {
667
783
  } catch {
668
784
  continue;
669
785
  }
670
- else parsed = raw ?? {};
786
+ else parsed = raw;
671
787
  const parsedObj = typeof parsed === "object" && parsed !== null ? parsed : {};
672
788
  const name = typeof parsedObj.name === "string" ? parsedObj.name : typeof parsedObj.skill === "string" ? parsedObj.skill : "";
673
- if (name) names.add(name);
789
+ if (name) callNames.set(event.data.callId, name);
790
+ } else if (event.type === "tool/result") {
791
+ const blocks = event.data?.message?.content;
792
+ if (!Array.isArray(blocks)) continue;
793
+ for (const block of blocks) {
794
+ const typed = block;
795
+ if (typed.type === "tool-result" && typed.isError !== true && typeof typed.toolCallId === "string") okCallIds.add(typed.toolCallId);
796
+ }
674
797
  }
798
+ const names = /* @__PURE__ */ new Set();
799
+ for (const [callId, name] of callNames) if (okCallIds.has(callId)) names.add(name);
675
800
  return names;
676
801
  }
677
802
  /** Map/set size that triggers a dead-session counter sweep (bounded, not a hard cap). */
@@ -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.67",
4
+ "version": "0.3.69",
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.67",
35
- "@lmzhen/dsh-evolution-core": "^0.3.67",
36
- "@lmzhen/dsh-evolution-plan-validator": "^0.3.67"
34
+ "@lmzhen/dsh-evolution-approval": "^0.3.69",
35
+ "@lmzhen/dsh-evolution-core": "^0.3.69",
36
+ "@lmzhen/dsh-evolution-plan-validator": "^0.3.69"
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.67",
46
- "@lmzhen/dsh-evolution-policy": "^0.3.67"
45
+ "@lmzhen/dsh-evolution-state": "^0.3.69",
46
+ "@lmzhen/dsh-evolution-policy": "^0.3.69"
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.67",
58
- "@lmzhen/dsh-evolution-core": "^0.3.67",
59
- "@lmzhen/dsh-evolution-plan-validator": "^0.3.67",
60
- "@lmzhen/dsh-evolution-state": "^0.3.67"
57
+ "@lmzhen/dsh-evolution-approval": "^0.3.69",
58
+ "@lmzhen/dsh-evolution-core": "^0.3.69",
59
+ "@lmzhen/dsh-evolution-plan-validator": "^0.3.69",
60
+ "@lmzhen/dsh-evolution-state": "^0.3.69"
61
61
  }
62
62
  }