@lmzhen/dsh-evolution-review 0.26.26 → 0.27.0

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
@@ -15,6 +15,7 @@ own, and the delivery mode lives on the `evolution-policy` row, not here.
15
15
 
16
16
  - **The autonomous write leg does not pass `tools.guard`.** `executePlan` calls the service seam directly (`memory.applyBatch`, `library.*`), so the guard `evolution-threat` installs on the TOOL channel never sees these writes (`grep "tools.execute(" src` = 0 by design). The scan still happens one layer down: `MemoryStore` and `SkillLibrary` refuse threat-shaped payloads at the write point through core's own pattern table, which is the same table the guard consumes. A deployment that wants the tool channel's guard wording on a plan write should route it through a tool — nothing in the plan path claims that coverage.
17
17
  - The review's model/context routing below applies to `reviewMode: 'subagent'` (opt-in since 0.3.74); in the default `'inject'` mode the review runs on the parent agent's model inside the parent thread.
18
+ - Review subagents run under a **restricted preset** (a read-only child preset): the tool surface is the caller's `reviewToolAllow` (schema default `[skill]`), and the child scope is the only place the allow-list is applied. A review run never inherits write authority from the parent session.
18
19
  - 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).
19
20
  - 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.
20
21
  - 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.
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- 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, advanceReview, assertSkillsRootAliasRetired, authorityFrom, clampOnce, clearReviewChannel, contentHash, evolutionIoAdapter, filterUnreadSkillOps, filterUnreadSkillOps as filterUnreadSkillOps$1, foldUntrustedLines, isPresent, isUnknown, liveBool, markReviewChannel, newSkillLibrary, paramRowId, payloadText, pickWithPolicy, policyStageLimits, readDispatchSignal, redactSecrets, resolveOrigins, resolveRootConfig, reviewPrompt, sessionAudited, sessionEvidenceIndex, sessionReadNames, sessionTurnSignals, sweepReviewChannelSessions, unwrapLive, userSetKeys, verifyPromptBundle } from "@lmzhen/dsh-evolution-core";
1
+ 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, advanceReview, assertSkillsRootAliasRetired, authorityFrom, clampOnce, classifySessionEvent, clearReviewChannel, contentHash, createSubagentChannel, evolutionIoAdapter, filterUnreadSkillOps, filterUnreadSkillOps as filterUnreadSkillOps$1, foldUntrustedLines, isPresent, isUnknown, liveBool, markReviewChannel, newSkillLibrary, paramRowId, payloadText, pickWithPolicy, policyStageLimits, readDispatchSignal, redactSecrets, resolveOrigins, resolveRootConfig, reviewPrompt, sessionAudited, sessionEvidenceIndex, sessionReadNames, sessionTurnSignals, sweepReviewChannelSessions, unwrapLive, userSetKeys, verifyPromptBundle } from "@lmzhen/dsh-evolution-core";
2
2
  import { createHash, randomUUID } from "node:crypto";
3
3
  import z from "@deepseek-ai/schemastery";
4
4
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
@@ -140,9 +140,7 @@ function emitReviewScheduled(ctx, event) {
140
140
  * 2026-09-16) and caps the sum at the timer ceiling (PLAN-R2 P2-1). The
141
141
  * `run.dispose` arm point counts this margin ALONE — its clock starts after
142
142
  * the result settled, not at the review timeout (PLAN-R2 P2-2, 2026-09-16). */
143
- const REVIEW_SETTLE_MARGIN_MS = 5e3;
144
143
  /** Error name marking the S2-6 watchdog expiry (see the catch in trySubagentReview). */
145
- const REVIEW_SETTLE_TIMEOUT = "ReviewSettleTimeout";
146
144
  const name = "evolution-review";
147
145
  const inject = ["agents"];
148
146
  const Config = z.object({
@@ -249,6 +247,7 @@ function apply(ctx, rawConfig = {}) {
249
247
  });
250
248
  if (!verifyPromptBundle(PROMPT_BUNDLE)) throw new Error("dsh-evolution prompt bundle integrity check failed; refusing to schedule review work");
251
249
  const config = clampReviewConfig(rawConfig, ctx);
250
+ const reviewChannel = createSubagentChannel(ctx);
252
251
  assertSkillsRootAliasRetired(rawConfig);
253
252
  const rootConfig = resolveRootConfig(rawConfig);
254
253
  const sessionState = new SessionScopedState();
@@ -263,6 +262,8 @@ function apply(ctx, rawConfig = {}) {
263
262
  const pendingReviewNotices = sessionState.add("pendingReviewNotices", /* @__PURE__ */ new Map());
264
263
  const cadenceResetWarned = sessionState.add("cadenceResetWarned", /* @__PURE__ */ new Set());
265
264
  let reviewInFlight = false;
265
+ /** W5 G3-S3: capability-missing is warned once per plugin instance (the error signal is per trigger). */
266
+ let channelUnavailableWarned = false;
266
267
  const policy = () => ctx.get("evolutionPolicy")?.get();
267
268
  /**
268
269
  * This row's knobs as they stand right now (G1 §8.3): the live fields are resolved
@@ -334,6 +335,7 @@ function apply(ctx, rawConfig = {}) {
334
335
  }
335
336
  ctx.on("session/event", (session, event) => {
336
337
  if (!sessionAudited(ctx, session.id, config.sessionScoped)) return;
338
+ if (classifySessionEvent(event) === "plugin-record") return;
337
339
  appendDispatchFrame(dispatchDigest, session.id, event);
338
340
  if (event.type === "user/message") {
339
341
  if ((event.data?.source)?.kind === "user") clearReviewChannel(session.id);
@@ -666,31 +668,6 @@ function apply(ctx, rawConfig = {}) {
666
668
  return false;
667
669
  }
668
670
  };
669
- let settleBudgetCapWarned = false;
670
- const resultSettleBudgetMs = () => {
671
- const budget = config.reviewTimeoutMs + Math.min(REVIEW_SETTLE_MARGIN_MS, config.reviewTimeoutMs);
672
- if (budget <= MAX_TIMER_DELAY_MS) return budget;
673
- if (!settleBudgetCapWarned) {
674
- settleBudgetCapWarned = true;
675
- ctx.logger.warn(`dsh-evolution-review: reviewTimeoutMs ${config.reviewTimeoutMs}ms plus the settle margin exceeds the 32-bit timer delay ceiling (${MAX_TIMER_DELAY_MS}ms) — the settle watchdog arms at the ceiling`);
676
- }
677
- return MAX_TIMER_DELAY_MS;
678
- };
679
- const disposeSettleBudgetMs = () => Math.min(REVIEW_SETTLE_MARGIN_MS, config.reviewTimeoutMs);
680
- const withSettleWatchdog = (promise, label, budgetMs, overdue) => new Promise((resolve, reject) => {
681
- const timer = setTimeout(() => {
682
- const error = /* @__PURE__ */ new Error(`dsh-evolution-review: ${label} ${overdue(budgetMs)} — abandoning the handle`);
683
- error.name = REVIEW_SETTLE_TIMEOUT;
684
- reject(error);
685
- }, budgetMs);
686
- promise.then((value) => {
687
- clearTimeout(timer);
688
- resolve(value);
689
- }, (error) => {
690
- clearTimeout(timer);
691
- reject(error instanceof Error ? error : new Error(String(error)));
692
- });
693
- });
694
671
  /**
695
672
  * T3-09/A37: the ONE abort channel for this fiber's long work.
696
673
  *
@@ -743,8 +720,19 @@ function apply(ctx, rawConfig = {}) {
743
720
  }
744
721
  async function trySubagentReview(session, agent, kind, signal) {
745
722
  if (params().reviewMode === "inject") return false;
746
- const subagents = ctx.get("subagents");
747
- if (!subagents) return false;
723
+ const channelState = reviewChannel.state;
724
+ if (channelState.kind !== "available") {
725
+ if (!channelUnavailableWarned) {
726
+ channelUnavailableWarned = true;
727
+ ctx.logger.warn("dsh-evolution-review: subagent channel unavailable (" + channelState.code + ": " + channelState.reason + ") — reviews fall back to the inject path until the capability returns");
728
+ }
729
+ try {
730
+ ctx.emit("evolution/review-error", { sessionId: session.id });
731
+ } catch (emitError) {
732
+ ctx.logger.warn("dsh-evolution-review: review-error emit failed: " + (emitError instanceof Error ? emitError.message : String(emitError)));
733
+ }
734
+ return false;
735
+ }
748
736
  if (reviewInFlight) {
749
737
  const existing = deferredFallbackReviews.findIndex((entry) => entry.sessionId === session.id && entry.kind === kind);
750
738
  if (existing >= 0 || deferredFallbackReviews.length < DEFERRED_REVIEW_CAP) {
@@ -771,138 +759,139 @@ function apply(ctx, rawConfig = {}) {
771
759
  const reviewText = redactSecrets(buildReviewRequest(session, kind, signal, config.reviewContextMessages, config.reviewMessageChars, dispatchDigest.get(session.id) ?? []));
772
760
  const agentOptions = { model };
773
761
  if (config.reviewProvider) agentOptions.provider = config.reviewProvider;
774
- const registry = ctx.get("tools");
775
762
  const requestedReviewTools = config.reviewToolAllow ?? [];
776
- if (requestedReviewTools.length > 0) if (registry === void 0) ctx.logger.warn("dsh-evolution-review: tools service not mounted — the review subagent tool allow-list was not probed; passing it through unchanged");
777
- else {
778
- const notInGlobalLayer = requestedReviewTools.filter((name) => registry.get(name) == null);
779
- if (notInGlobalLayer.length > 0) ctx.logger.warn(`dsh-evolution-review: reviewToolAllow name(s) ${notInGlobalLayer.map((name) => `"${name}"`).join(", ")} are absent from the GLOBAL tool layer — passing them through unchanged (a preset-mounted tool is invisible to a scope-less lookup)`);
780
- }
781
763
  const preRunHashes = await treeSkillHashes();
782
764
  const sessionSeqAtPlanTime = session.seq - 1;
783
765
  const substantiveEvidenceSeqs = sessionEvidenceIndex(ctx, session);
784
- const run = await subagents.start("spawn", {
785
- label: "dsh-evolution-review",
786
- prompt: [{
787
- type: "text",
788
- text: reviewText
789
- }],
766
+ const outcome = await reviewChannel.run({
790
767
  parent: agent,
791
- signal: AbortSignal.any([AbortSignal.timeout(config.reviewTimeoutMs), fiberAbort.signal]),
768
+ label: "dsh-evolution-review",
769
+ promptText: reviewText + "\n\n" + reviewPrompt(kind, "plan"),
770
+ kind: "reviewer",
771
+ allow: requestedReviewTools,
772
+ outputSchema: REVIEW_OUTPUT_SCHEMA,
792
773
  maxDepth: config.reviewMaxDepth,
793
774
  agentOptions,
794
- persona: reviewPrompt(kind, "plan"),
795
- toolFilter: { allow: requestedReviewTools },
796
- outputSchema: REVIEW_OUTPUT_SCHEMA
775
+ timeoutMs: config.reviewTimeoutMs,
776
+ signal: fiberAbort.signal,
777
+ afterResult: (child) => {
778
+ const childSession = child.session;
779
+ return childSession === void 0 ? /* @__PURE__ */ new Set() : new Set(sessionReadNames(ctx, childSession) ?? []);
780
+ }
797
781
  });
798
- try {
799
- const result = await withSettleWatchdog(run.result, "subagent review result", resultSettleBudgetMs(), (total) => `did not settle within ${Math.max(0, total - config.reviewTimeoutMs)}ms after the review timeout (watchdog total ${total}ms)`);
800
- if (!result.structured) {
801
- try {
802
- ctx.emit("evolution/review-error", { sessionId: session.id });
803
- } catch (emitError) {
804
- ctx.logger.warn(`dsh-evolution-review: review-error emit failed: ${emitError instanceof Error ? emitError.message : String(emitError)}`);
805
- }
806
- const stopDetail = typeof result.stopReason === "string" ? result.stopReason : void 0;
807
- const diagDetail = typeof result.diagnostic === "string" ? result.diagnostic : void 0;
808
- ctx.logger.warn(`dsh-evolution-review: review subagent returned no structured plan${stopDetail !== void 0 ? ` (stopReason=${stopDetail}${diagDetail !== void 0 ? `; diagnostic=${diagDetail}` : ""})` : ""}`);
809
- return false;
782
+ if (outcome.kind === "aborted") {
783
+ try {
784
+ ctx.emit("evolution/review-error", { sessionId: session.id });
785
+ } catch (emitError) {
786
+ ctx.logger.warn("dsh-evolution-review: review-error emit failed: " + (emitError instanceof Error ? emitError.message : String(emitError)));
787
+ }
788
+ return false;
789
+ }
790
+ if (outcome.kind === "unavailable") {
791
+ if (!channelUnavailableWarned) {
792
+ channelUnavailableWarned = true;
793
+ ctx.logger.warn("dsh-evolution-review: subagent channel unavailable (" + outcome.code + ": " + outcome.reason + ") — reviews fall back to the inject path until the capability returns");
810
794
  }
811
- const childReads = run.localAgent ? sessionReadNames(ctx, run.localAgent.session) ?? /* @__PURE__ */ new Set() : /* @__PURE__ */ new Set();
812
- const plan = result.structured;
813
- const policyFingerprint = fingerprintPolicy(snapshot);
814
- const validation = validateEvolutionPlan(plan, {
815
- sessionSeq: sessionSeqAtPlanTime,
816
- substantiveEvidenceSeqs,
817
- maxOpsPerPlan: snapshot?.maxOpsPerPlan ?? DEFAULT_MAX_OPS_PER_PLAN,
818
- protectedSkillNames: new Set(snapshot?.protectedSkillNames ?? []),
819
- maxMemoryChars: snapshot?.memoryChars ?? DEFAULT_MEMORY_CHAR_LIMIT,
820
- maxUserChars: snapshot?.userChars ?? DEFAULT_USER_CHAR_LIMIT,
821
- maxSkillContentChars: snapshot?.skillContentChars ?? DEFAULT_SKILL_CONTENT_CHARS
822
- });
823
- if (validation.reports.length > 0) ctx.logger.warn(`dsh-evolution-review: ${validation.reports.length} op(s) cite only bookkeeping frames as evidence: ${validation.reports.map((report) => report.reason).join("; ")}`);
824
- const acceptedSkillOps = validation.accepted.skillOps ?? [];
825
- const skippedUnread = filterUnreadSkillOps$1(acceptedSkillOps, new Set([...sessionReadNames(ctx, session) ?? /* @__PURE__ */ new Set(), ...childReads]));
826
- const evidenceQuotes = [...validation.accepted.memoryOps ?? [], ...acceptedSkillOps].reduce((total, op) => total + (Array.isArray(op.evidence) ? op.evidence.length : 0), 0);
827
- const emitApplied = (report) => {
828
- try {
829
- ctx.emit("evolution/plan-applied", {
830
- sessionId: session.id,
831
- planId: randomUUID(),
832
- policyFingerprint,
833
- memoryApplied: report.actions.filter((action) => action.startsWith("Memory")).length,
834
- skillApplied: report.actions.filter((action) => action.startsWith("Skill ")).length,
835
- rejectedOps: validation.rejected.length,
836
- ...skippedUnread > 0 ? { skippedUnread } : {},
837
- ...validation.reports.length > 0 ? { evidenceClassReports: validation.reports.length } : {},
838
- executionFailures: report.failedOps?.length ?? 0,
839
- ...report.executionError !== void 0 ? { executionError: report.executionError } : {},
840
- evidenceQuotes,
841
- estimatedInputChars: reviewText.length
842
- });
843
- } catch (emitError) {
844
- ctx.logger.warn(`dsh-evolution-review: plan-applied emit failed: ${emitError instanceof Error ? emitError.message : String(emitError)}`);
845
- }
846
- };
847
- const landed = [];
848
- let deadlineMissed = false;
849
- const lateLandings = [];
850
- const noteLanded = (action) => {
851
- landed.push(action);
852
- if (!deadlineMissed) return;
853
- lateLandings.push(action);
854
- const before = landed.length - lateLandings.length;
855
- const earlier = before > 0 ? ` (${before} other op(s) had landed before the deadline)` : "";
856
- deliverMessage(agent, `\u{1F4BE} Self-improvement review: ${action} ${lateLandings.length === 1 ? "landed AFTER the review timed out" : `landed after the timeout (${lateLandings.length} late so far)`}${earlier} \u2014 reconcile before re-requesting the same change.`, "self-improvement review late landing");
857
- };
858
- let executed;
859
795
  try {
860
- executed = await withTimeout(executePlan(validation.accepted, session, noteLanded, preRunHashes, fiberAbort.signal), config.reviewTimeoutMs, "review plan execution");
861
- } catch (error) {
862
- deadlineMissed = true;
863
- emitApplied({
864
- actions: landed,
865
- executionError: `execution timed out after ${config.reviewTimeoutMs}ms`
866
- });
867
- throw error;
796
+ ctx.emit("evolution/review-error", { sessionId: session.id });
797
+ } catch (emitError) {
798
+ ctx.logger.warn("dsh-evolution-review: review-error emit failed: " + (emitError instanceof Error ? emitError.message : String(emitError)));
868
799
  }
869
- const actions = executed.actions;
870
- if (actions.length > 0) {
871
- const applied = actions.join(" · ");
872
- const failedNote = executed.failedOps.length > 0 ? ` 失败 ${executed.failedOps.length} 个:${executed.failedOps.join(";")}。` : "";
873
- deliverMessage(agent, `💾 Self-improvement review: ${applied}${executed.ok ? "" : `\n部分操作失败。${failedNote}以上操作已应用,请勿重复执行。`}`, "self-improvement review");
874
- } else {
875
- const reasons = [];
876
- if (validation.rejected.length > 0) reasons.push(`${validation.rejected.length} op(s) rejected by validation`);
877
- if (skippedUnread > 0) reasons.push(`${skippedUnread} op(s) skipped (skill not read this session)`);
878
- if (executed.failedOps.length > 0) reasons.push(`${executed.failedOps.length} op(s) failed at execution: ${executed.failedOps.join("; ")}`);
879
- if (executed.aborted !== void 0) reasons.push(`execution aborted: ${executed.aborted}`);
880
- if (reasons.length === 0) reasons.push("the review plan contained nothing executable");
881
- let text = `💾 Self-improvement review: 0 ops landed. ${reasons.join(" ")}`;
882
- if (text.length > 500) text = `${text.slice(0, 497)}…`;
883
- deliverMessage(agent, text, "self-improvement review");
800
+ return false;
801
+ }
802
+ if (outcome.kind === "no-structured") {
803
+ try {
804
+ ctx.emit("evolution/review-error", { sessionId: session.id });
805
+ } catch (emitError) {
806
+ ctx.logger.warn("dsh-evolution-review: review-error emit failed: " + (emitError instanceof Error ? emitError.message : String(emitError)));
884
807
  }
885
- emitApplied({
886
- actions,
887
- failedOps: executed.failedOps,
888
- ...executed.aborted !== void 0 ? { executionError: executed.aborted } : executed.failedOps[0] !== void 0 ? { executionError: executed.failedOps[0] } : {}
889
- });
890
- return true;
891
- } finally {
808
+ const stopDetail = typeof outcome.stopReason === "string" ? outcome.stopReason : void 0;
809
+ const diagDetail = typeof outcome.diagnostic === "string" ? outcome.diagnostic : void 0;
810
+ ctx.logger.warn("dsh-evolution-review: review subagent returned no structured plan" + (stopDetail !== void 0 ? " (stopReason=" + stopDetail + (diagDetail !== void 0 ? "; " + diagDetail : "") + ")" : ""));
811
+ return false;
812
+ }
813
+ const childReads = outcome.evidence ?? /* @__PURE__ */ new Set();
814
+ const plan = outcome.structured;
815
+ const policyFingerprint = fingerprintPolicy(snapshot);
816
+ const validation = validateEvolutionPlan(plan, {
817
+ sessionSeq: sessionSeqAtPlanTime,
818
+ substantiveEvidenceSeqs,
819
+ maxOpsPerPlan: snapshot?.maxOpsPerPlan ?? DEFAULT_MAX_OPS_PER_PLAN,
820
+ protectedSkillNames: new Set(snapshot?.protectedSkillNames ?? []),
821
+ maxMemoryChars: snapshot?.memoryChars ?? DEFAULT_MEMORY_CHAR_LIMIT,
822
+ maxUserChars: snapshot?.userChars ?? DEFAULT_USER_CHAR_LIMIT,
823
+ maxSkillContentChars: snapshot?.skillContentChars ?? DEFAULT_SKILL_CONTENT_CHARS
824
+ });
825
+ if (validation.reports.length > 0) ctx.logger.warn(`dsh-evolution-review: ${validation.reports.length} op(s) cite only bookkeeping frames as evidence: ${validation.reports.map((report) => report.reason).join("; ")}`);
826
+ const acceptedSkillOps = validation.accepted.skillOps ?? [];
827
+ const skippedUnread = filterUnreadSkillOps$1(acceptedSkillOps, new Set([...sessionReadNames(ctx, session) ?? /* @__PURE__ */ new Set(), ...childReads]));
828
+ const evidenceQuotes = [...validation.accepted.memoryOps ?? [], ...acceptedSkillOps].reduce((total, op) => total + (Array.isArray(op.evidence) ? op.evidence.length : 0), 0);
829
+ const emitApplied = (report) => {
892
830
  try {
893
- await withSettleWatchdog(run.dispose(), "subagent dispose", disposeSettleBudgetMs(), (total) => `did not settle within ${total}ms`);
894
- } catch (disposeError) {
895
- ctx.logger.warn(`dsh-evolution-review: subagent dispose failed: ${disposeError instanceof Error ? disposeError.message : String(disposeError)}`);
831
+ ctx.emit("evolution/plan-applied", {
832
+ sessionId: session.id,
833
+ planId: randomUUID(),
834
+ policyFingerprint,
835
+ memoryApplied: report.actions.filter((action) => action.startsWith("Memory")).length,
836
+ skillApplied: report.actions.filter((action) => action.startsWith("Skill ")).length,
837
+ rejectedOps: validation.rejected.length,
838
+ ...skippedUnread > 0 ? { skippedUnread } : {},
839
+ ...validation.reports.length > 0 ? { evidenceClassReports: validation.reports.length } : {},
840
+ executionFailures: report.failedOps?.length ?? 0,
841
+ ...report.executionError !== void 0 ? { executionError: report.executionError } : {},
842
+ evidenceQuotes,
843
+ estimatedInputChars: reviewText.length
844
+ });
845
+ } catch (emitError) {
846
+ ctx.logger.warn(`dsh-evolution-review: plan-applied emit failed: ${emitError instanceof Error ? emitError.message : String(emitError)}`);
896
847
  }
848
+ };
849
+ const landed = [];
850
+ let deadlineMissed = false;
851
+ const lateLandings = [];
852
+ const noteLanded = (action) => {
853
+ landed.push(action);
854
+ if (!deadlineMissed) return;
855
+ lateLandings.push(action);
856
+ const before = landed.length - lateLandings.length;
857
+ const earlier = before > 0 ? ` (${before} other op(s) had landed before the deadline)` : "";
858
+ deliverMessage(agent, `\u{1F4BE} Self-improvement review: ${action} ${lateLandings.length === 1 ? "landed AFTER the review timed out" : `landed after the timeout (${lateLandings.length} late so far)`}${earlier} \u2014 reconcile before re-requesting the same change.`, "self-improvement review late landing");
859
+ };
860
+ let executed;
861
+ try {
862
+ executed = await withTimeout(executePlan(validation.accepted, session, noteLanded, preRunHashes, fiberAbort.signal), config.reviewTimeoutMs, "review plan execution");
863
+ } catch (error) {
864
+ deadlineMissed = true;
865
+ emitApplied({
866
+ actions: landed,
867
+ executionError: `execution timed out after ${config.reviewTimeoutMs}ms`
868
+ });
869
+ throw error;
870
+ }
871
+ const actions = executed.actions;
872
+ if (actions.length > 0) {
873
+ const applied = actions.join(" · ");
874
+ const failedNote = executed.failedOps.length > 0 ? ` 失败 ${executed.failedOps.length} 个:${executed.failedOps.join(";")}。` : "";
875
+ deliverMessage(agent, `💾 Self-improvement review: ${applied}${executed.ok ? "" : `\n部分操作失败。${failedNote}以上操作已应用,请勿重复执行。`}`, "self-improvement review");
876
+ } else {
877
+ const reasons = [];
878
+ if (validation.rejected.length > 0) reasons.push(`${validation.rejected.length} op(s) rejected by validation`);
879
+ if (skippedUnread > 0) reasons.push(`${skippedUnread} op(s) skipped (skill not read this session)`);
880
+ if (executed.failedOps.length > 0) reasons.push(`${executed.failedOps.length} op(s) failed at execution: ${executed.failedOps.join("; ")}`);
881
+ if (executed.aborted !== void 0) reasons.push(`execution aborted: ${executed.aborted}`);
882
+ if (reasons.length === 0) reasons.push("the review plan contained nothing executable");
883
+ let text = `💾 Self-improvement review: 0 ops landed. ${reasons.join(" ")}`;
884
+ if (text.length > 500) text = `${text.slice(0, 497)}…`;
885
+ deliverMessage(agent, text, "self-improvement review");
897
886
  }
887
+ emitApplied({
888
+ actions,
889
+ failedOps: executed.failedOps,
890
+ ...executed.aborted !== void 0 ? { executionError: executed.aborted } : executed.failedOps[0] !== void 0 ? { executionError: executed.failedOps[0] } : {}
891
+ });
892
+ return true;
898
893
  } catch (error) {
899
894
  ctx.logger.warn(`dsh-evolution-review: subagent review failed: ${error instanceof Error ? error.message : String(error)}`);
900
- if (error instanceof Error && error.name === REVIEW_SETTLE_TIMEOUT) try {
901
- ctx.emit("evolution/review-error", { sessionId: session.id });
902
- } catch (emitError) {
903
- ctx.logger.warn(`dsh-evolution-review: review-error emit failed: ${emitError instanceof Error ? emitError.message : String(emitError)}`);
904
- }
905
- if (error instanceof Error && error.message.includes("plan execution timed out")) ctx.logger.warn("dsh-evolution-review: plan execution abandoned on timeout — late writes ARE reported to the model as they land (S2-10) but still have NO plan-applied record and race the fallback inject; inspect the skill tree and usage sidecar");
906
895
  return false;
907
896
  } finally {
908
897
  reviewInFlight = false;
@@ -15,6 +15,14 @@ declare module '@deepseek-ai/dsh-llm' {
15
15
  import type { Session } from '@deepseek-ai/dsh-session';
16
16
  import { type ReviewKind } from '@lmzhen/dsh-evolution-core';
17
17
  export { filterUnreadSkillOps } from '@lmzhen/dsh-evolution-core';
18
+ /** S2-6 (FLOW1-1): the settle-grace margin a subagent handle gets beyond its
19
+ * own deadline before the review abandons it. As the dispose watchdog's WHOLE
20
+ * budget it is capped by the review timeout so a short (test) budget stays
21
+ * short; the result watchdog adds it AFTER the full timeout (PLAN S1.1,
22
+ * 2026-09-16) and caps the sum at the timer ceiling (PLAN-R2 P2-1). The
23
+ * `run.dispose` arm point counts this margin ALONE — its clock starts after
24
+ * the result settled, not at the review timeout (PLAN-R2 P2-2, 2026-09-16). */
25
+ /** Error name marking the S2-6 watchdog expiry (see the catch in trySubagentReview). */
18
26
  export declare const name = "evolution-review";
19
27
  export declare const inject: string[];
20
28
  export interface 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.26.26",
4
+ "version": "0.27.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -26,32 +26,35 @@
26
26
  ],
27
27
  "license": "MIT",
28
28
  "dependencies": {
29
- "@deepseek-ai/schemastery": "^3.18.4",
30
- "@lmzhen/dsh-evolution-approval": "^0.26.26",
31
- "@lmzhen/dsh-evolution-core": "^0.26.26",
32
- "@lmzhen/dsh-evolution-plan-validator": "^0.26.26"
29
+ "@deepseek-ai/schemastery": "^3.18.5",
30
+ "@lmzhen/dsh-evolution-approval": "^0.27.0",
31
+ "@lmzhen/dsh-evolution-core": "^0.27.0",
32
+ "@lmzhen/dsh-evolution-plan-validator": "^0.27.0"
33
33
  },
34
34
  "peerDependencies": {
35
- "@deepseek-ai/cordis": "^4.0.1",
36
- "@deepseek-ai/dsh-agent": "^0.2.0-rc.1",
37
- "@deepseek-ai/dsh-llm": "^0.2.0-rc.1",
38
- "@deepseek-ai/dsh-session": "^0.2.0-rc.1",
39
- "@deepseek-ai/dsh-tools": "^0.2.0-rc.1",
40
- "@lmzhen/dsh-evolution-policy": "^0.26.26",
41
- "@lmzhen/dsh-evolution-state": "^0.26.26"
35
+ "@deepseek-ai/cordis": "^4.0.5",
36
+ "@deepseek-ai/dsh-agent": "^0.2.1-rc.1",
37
+ "@deepseek-ai/dsh-llm": "^0.2.1-rc.1",
38
+ "@deepseek-ai/dsh-session": "^0.2.1-rc.1",
39
+ "@deepseek-ai/dsh-tools": "^0.2.1-rc.1",
40
+ "@lmzhen/dsh-evolution-policy": "^0.27.0",
41
+ "@lmzhen/dsh-evolution-state": "^0.27.0"
42
42
  },
43
43
  "devDependencies": {
44
- "@deepseek-ai/dsh-agent": "^0.2.0-rc.1",
45
- "@deepseek-ai/dsh-agent-loop-testkit": "^0.2.0-rc.1",
46
- "@deepseek-ai/dsh-llm": "^0.2.0-rc.1",
47
- "@deepseek-ai/dsh-session": "^0.2.0-rc.1",
48
- "@deepseek-ai/dsh-session-persistence": "^0.2.0-rc.1",
49
- "@deepseek-ai/dsh-session-persistence-jsonl": "^0.2.0-rc.1",
50
- "@deepseek-ai/dsh-tools": "^0.2.0-rc.1",
51
- "@lmzhen/dsh-evolution-approval": "^0.26.26",
52
- "@lmzhen/dsh-evolution-core": "^0.26.26",
53
- "@lmzhen/dsh-evolution-curator": "^0.26.26",
54
- "@lmzhen/dsh-evolution-plan-validator": "^0.26.26",
55
- "@lmzhen/dsh-evolution-state": "^0.26.26"
44
+ "@deepseek-ai/dsh-agent": "^0.2.1-rc.1",
45
+ "@deepseek-ai/dsh-agent-loop-testkit": "^0.2.1-rc.1",
46
+ "@deepseek-ai/dsh-llm": "^0.2.1-rc.1",
47
+ "@deepseek-ai/dsh-session": "^0.2.1-rc.1",
48
+ "@deepseek-ai/dsh-session-persistence": "^0.2.1-rc.1",
49
+ "@deepseek-ai/dsh-session-persistence-jsonl": "^0.2.1-rc.1",
50
+ "@deepseek-ai/dsh-tools": "^0.2.1-rc.1",
51
+ "@lmzhen/dsh-evolution-approval": "^0.27.0",
52
+ "@lmzhen/dsh-evolution-core": "^0.27.0",
53
+ "@lmzhen/dsh-evolution-curator": "^0.27.0",
54
+ "@lmzhen/dsh-evolution-plan-validator": "^0.27.0",
55
+ "@lmzhen/dsh-evolution-state": "^0.27.0"
56
+ },
57
+ "engines": {
58
+ "dsh": ">=0.2.1-rc.1 <0.2.2-0"
56
59
  }
57
60
  }