@lmzhen/dsh-evolution-review 0.3.68 → 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.
Files changed (2) hide show
  1. package/lib/index.js +119 -14
  2. package/package.json +10 -10
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_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";
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
  /**
@@ -71,6 +71,12 @@ function clampReviewConfig(rawConfig, ctx) {
71
71
  if (clamped.length > 0) ctx.logger.warn(`dsh-evolution-review: ${clamped.join(", ")} provided an invalid value; falling back to the default`);
72
72
  return config;
73
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
+ }
74
80
  function apply(ctx, rawConfig = {}) {
75
81
  if (!verifyPromptBundle(PROMPT_BUNDLE)) throw new Error("dsh-evolution prompt bundle integrity check failed; refusing to schedule review work");
76
82
  const config = clampReviewConfig(rawConfig, ctx);
@@ -325,6 +331,26 @@ function apply(ctx, rawConfig = {}) {
325
331
  ctx.effect(() => () => {
326
332
  deferredFallbackReviews.length = 0;
327
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
+ }
328
354
  async function trySubagentReview(session, agent, kind, signal) {
329
355
  if ((policy()?.reviewMode ?? config.reviewMode) === "inject") return false;
330
356
  const subagents = ctx.get("subagents");
@@ -352,6 +378,7 @@ function apply(ctx, rawConfig = {}) {
352
378
  const reviewText = redactSecrets(buildReviewRequest(session, kind, signal, config.reviewContextMessages, config.reviewMessageChars));
353
379
  const agentOptions = { model };
354
380
  if (config.reviewProvider) agentOptions.provider = config.reviewProvider;
381
+ const preRunHashes = await treeSkillHashes();
355
382
  const run = await subagents.start("spawn", {
356
383
  label: "dsh-evolution-review",
357
384
  prompt: [{
@@ -413,7 +440,7 @@ function apply(ctx, rawConfig = {}) {
413
440
  const landed = [];
414
441
  let executed;
415
442
  try {
416
- executed = await withTimeout(executePlan(validation.accepted, session, (action) => landed.push(action)), config.reviewTimeoutMs, "review plan execution");
443
+ executed = await withTimeout(executePlan(validation.accepted, session, (action) => landed.push(action), preRunHashes), config.reviewTimeoutMs, "review plan execution");
417
444
  } catch (error) {
418
445
  emitApplied({
419
446
  actions: landed,
@@ -424,7 +451,8 @@ function apply(ctx, rawConfig = {}) {
424
451
  const actions = executed.actions;
425
452
  if (actions.length > 0) {
426
453
  const applied = actions.join(" · ");
427
- 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}以上操作已应用,请勿重复执行。`;
428
456
  try {
429
457
  deliverMessage(agent, `💾 Self-improvement review: ${applied}${note}`, "self-improvement review");
430
458
  } catch (injectError) {
@@ -486,7 +514,7 @@ function apply(ctx, rawConfig = {}) {
486
514
  }
487
515
  }
488
516
  }
489
- async function executePlan(plan, session, onLanded) {
517
+ async function executePlan(plan, session, onLanded, preRunHashes) {
490
518
  const sessionId = session?.id;
491
519
  const memory = ctx.get("memory");
492
520
  const approval = ctx.get("evolutionApproval");
@@ -535,6 +563,21 @@ function apply(ctx, rawConfig = {}) {
535
563
  const stageCurrent = await hashLibrary.read(args.name).catch(() => null);
536
564
  if (stageCurrent !== null) args.staged_from_sha256 = contentHash(stageCurrent);
537
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
+ }
538
581
  const runnerArgs = {
539
582
  operation: args,
540
583
  origin: origins.library
@@ -543,6 +586,10 @@ function apply(ctx, rawConfig = {}) {
543
586
  if (result?.ok) {
544
587
  actions.push(`Skill ${op.name} ${op.action ?? "patch"}`);
545
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
+ }
546
593
  } else {
547
594
  ok = false;
548
595
  failedOps.push(`skill ${op.action ?? "patch"} ${op.name}: ${result?.message ?? "service unavailable"}`);
@@ -617,19 +664,55 @@ function apply(ctx, rawConfig = {}) {
617
664
  ok: false,
618
665
  message: "evolution-io service not mounted"
619
666
  };
620
- 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) => {
621
672
  ctx.emit("evolution/skill-mutated", event);
622
673
  });
623
674
  const op = skillArgs;
624
675
  const name = op.name ?? "";
625
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
+ }
626
693
  if (op.action === "create") {
627
694
  const created = await library.create(name, op.content ?? "", origin);
628
695
  if (created.ok) await ctx.get("skillUsage")?.markAgentCreated?.(name);
629
696
  return created;
630
697
  }
631
- if (op.action === "edit" || op.action === "update") return await library.update(name, op.content ?? "", origin);
632
- 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
+ }
633
716
  if (op.action === "delete") {
634
717
  const into = (op.absorbed_into ?? "").trim();
635
718
  if (!into || !await library.read(into)) return {
@@ -640,8 +723,21 @@ function apply(ctx, rawConfig = {}) {
640
723
  if (archived.ok) await ctx.get("skillUsage")?.markArchived?.(name);
641
724
  return archived;
642
725
  }
643
- if (op.action === "write_file") return await library.writeSupportFile(name, op.file_path ?? "", op.file_content ?? "", origin);
644
- 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
+ }
645
741
  if (op.action === "restructure") {
646
742
  const moves = (op.restructure ?? []).filter((move) => move !== null).map((move) => ({
647
743
  heading: move.heading ?? "",
@@ -676,9 +772,9 @@ function shouldCompletionReview(reason, sessionToolCalls, minToolCalls) {
676
772
  * per-skill read action (its `list`/`review` are whole-library), so a specific
677
773
  * skill read through it cannot be tracked — see README Known Limitations. */
678
774
  function collectReadSkillNames(session) {
679
- const names = /* @__PURE__ */ new Set();
680
- for (const event of session.events) {
681
- 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") {
682
778
  if (event.data.name !== "skill") continue;
683
779
  const raw = event.data.arguments;
684
780
  let parsed = {};
@@ -687,11 +783,20 @@ function collectReadSkillNames(session) {
687
783
  } catch {
688
784
  continue;
689
785
  }
690
- else parsed = raw ?? {};
786
+ else parsed = raw;
691
787
  const parsedObj = typeof parsed === "object" && parsed !== null ? parsed : {};
692
788
  const name = typeof parsedObj.name === "string" ? parsedObj.name : typeof parsedObj.skill === "string" ? parsedObj.skill : "";
693
- 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
+ }
694
797
  }
798
+ const names = /* @__PURE__ */ new Set();
799
+ for (const [callId, name] of callNames) if (okCallIds.has(callId)) names.add(name);
695
800
  return names;
696
801
  }
697
802
  /** Map/set size that triggers a dead-session counter sweep (bounded, not a hard cap). */
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.68",
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.68",
35
- "@lmzhen/dsh-evolution-core": "^0.3.68",
36
- "@lmzhen/dsh-evolution-plan-validator": "^0.3.68"
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.68",
46
- "@lmzhen/dsh-evolution-policy": "^0.3.68"
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.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"
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
  }