@lmzhen/dsh-evolution-review 0.26.15 → 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 +1 -0
- package/lib/index.js +163 -152
- package/lib/types/index.d.ts +8 -0
- package/package.json +27 -24
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,8 +1,8 @@
|
|
|
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";
|
|
1
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
3
|
import z from "@deepseek-ai/schemastery";
|
|
3
4
|
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
4
5
|
import { SessionId } from "@deepseek-ai/dsh-session";
|
|
5
|
-
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, clampOnce, clearReviewChannel, contentHash, evolutionIoAdapter, filterUnreadSkillOps, filterUnreadSkillOps as filterUnreadSkillOps$1, foldUntrustedLines, 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";
|
|
6
6
|
import { validateEvolutionPlan } from "@lmzhen/dsh-evolution-plan-validator";
|
|
7
7
|
/**
|
|
8
8
|
* Whether a message is a notice this plugin delivered, judged only by durable source fields.
|
|
@@ -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
|
*
|
|
@@ -731,6 +708,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
731
708
|
const io = ctx.get("evolutionIo");
|
|
732
709
|
if (!io) return hashes;
|
|
733
710
|
const library = newSkillLibrary({
|
|
711
|
+
authority: authorityFrom({ origin: "foreground" }),
|
|
734
712
|
config: rootConfig,
|
|
735
713
|
io: evolutionIoAdapter(() => io.provider())
|
|
736
714
|
});
|
|
@@ -742,8 +720,19 @@ function apply(ctx, rawConfig = {}) {
|
|
|
742
720
|
}
|
|
743
721
|
async function trySubagentReview(session, agent, kind, signal) {
|
|
744
722
|
if (params().reviewMode === "inject") return false;
|
|
745
|
-
const
|
|
746
|
-
if (
|
|
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
|
+
}
|
|
747
736
|
if (reviewInFlight) {
|
|
748
737
|
const existing = deferredFallbackReviews.findIndex((entry) => entry.sessionId === session.id && entry.kind === kind);
|
|
749
738
|
if (existing >= 0 || deferredFallbackReviews.length < DEFERRED_REVIEW_CAP) {
|
|
@@ -770,138 +759,139 @@ function apply(ctx, rawConfig = {}) {
|
|
|
770
759
|
const reviewText = redactSecrets(buildReviewRequest(session, kind, signal, config.reviewContextMessages, config.reviewMessageChars, dispatchDigest.get(session.id) ?? []));
|
|
771
760
|
const agentOptions = { model };
|
|
772
761
|
if (config.reviewProvider) agentOptions.provider = config.reviewProvider;
|
|
773
|
-
const registry = ctx.get("tools");
|
|
774
762
|
const requestedReviewTools = config.reviewToolAllow ?? [];
|
|
775
|
-
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");
|
|
776
|
-
else {
|
|
777
|
-
const notInGlobalLayer = requestedReviewTools.filter((name) => registry.get(name) == null);
|
|
778
|
-
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)`);
|
|
779
|
-
}
|
|
780
763
|
const preRunHashes = await treeSkillHashes();
|
|
781
764
|
const sessionSeqAtPlanTime = session.seq - 1;
|
|
782
765
|
const substantiveEvidenceSeqs = sessionEvidenceIndex(ctx, session);
|
|
783
|
-
const
|
|
784
|
-
label: "dsh-evolution-review",
|
|
785
|
-
prompt: [{
|
|
786
|
-
type: "text",
|
|
787
|
-
text: reviewText
|
|
788
|
-
}],
|
|
766
|
+
const outcome = await reviewChannel.run({
|
|
789
767
|
parent: agent,
|
|
790
|
-
|
|
768
|
+
label: "dsh-evolution-review",
|
|
769
|
+
promptText: reviewText + "\n\n" + reviewPrompt(kind, "plan"),
|
|
770
|
+
kind: "reviewer",
|
|
771
|
+
allow: requestedReviewTools,
|
|
772
|
+
outputSchema: REVIEW_OUTPUT_SCHEMA,
|
|
791
773
|
maxDepth: config.reviewMaxDepth,
|
|
792
774
|
agentOptions,
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
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
|
+
}
|
|
796
781
|
});
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
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");
|
|
809
794
|
}
|
|
810
|
-
const childReads = run.localAgent ? sessionReadNames(ctx, run.localAgent.session) ?? /* @__PURE__ */ new Set() : /* @__PURE__ */ new Set();
|
|
811
|
-
const plan = result.structured;
|
|
812
|
-
const policyFingerprint = fingerprintPolicy(snapshot);
|
|
813
|
-
const validation = validateEvolutionPlan(plan, {
|
|
814
|
-
sessionSeq: sessionSeqAtPlanTime,
|
|
815
|
-
substantiveEvidenceSeqs,
|
|
816
|
-
maxOpsPerPlan: snapshot?.maxOpsPerPlan ?? DEFAULT_MAX_OPS_PER_PLAN,
|
|
817
|
-
protectedSkillNames: new Set(snapshot?.protectedSkillNames ?? []),
|
|
818
|
-
maxMemoryChars: snapshot?.memoryChars ?? DEFAULT_MEMORY_CHAR_LIMIT,
|
|
819
|
-
maxUserChars: snapshot?.userChars ?? DEFAULT_USER_CHAR_LIMIT,
|
|
820
|
-
maxSkillContentChars: snapshot?.skillContentChars ?? DEFAULT_SKILL_CONTENT_CHARS
|
|
821
|
-
});
|
|
822
|
-
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("; ")}`);
|
|
823
|
-
const acceptedSkillOps = validation.accepted.skillOps ?? [];
|
|
824
|
-
const skippedUnread = filterUnreadSkillOps$1(acceptedSkillOps, new Set([...sessionReadNames(ctx, session) ?? /* @__PURE__ */ new Set(), ...childReads]));
|
|
825
|
-
const evidenceQuotes = [...validation.accepted.memoryOps ?? [], ...acceptedSkillOps].reduce((total, op) => total + (Array.isArray(op.evidence) ? op.evidence.length : 0), 0);
|
|
826
|
-
const emitApplied = (report) => {
|
|
827
|
-
try {
|
|
828
|
-
ctx.emit("evolution/plan-applied", {
|
|
829
|
-
sessionId: session.id,
|
|
830
|
-
planId: randomUUID(),
|
|
831
|
-
policyFingerprint,
|
|
832
|
-
memoryApplied: report.actions.filter((action) => action.startsWith("Memory")).length,
|
|
833
|
-
skillApplied: report.actions.filter((action) => action.startsWith("Skill ")).length,
|
|
834
|
-
rejectedOps: validation.rejected.length,
|
|
835
|
-
...skippedUnread > 0 ? { skippedUnread } : {},
|
|
836
|
-
...validation.reports.length > 0 ? { evidenceClassReports: validation.reports.length } : {},
|
|
837
|
-
executionFailures: report.failedOps?.length ?? 0,
|
|
838
|
-
...report.executionError !== void 0 ? { executionError: report.executionError } : {},
|
|
839
|
-
evidenceQuotes,
|
|
840
|
-
estimatedInputChars: reviewText.length
|
|
841
|
-
});
|
|
842
|
-
} catch (emitError) {
|
|
843
|
-
ctx.logger.warn(`dsh-evolution-review: plan-applied emit failed: ${emitError instanceof Error ? emitError.message : String(emitError)}`);
|
|
844
|
-
}
|
|
845
|
-
};
|
|
846
|
-
const landed = [];
|
|
847
|
-
let deadlineMissed = false;
|
|
848
|
-
const lateLandings = [];
|
|
849
|
-
const noteLanded = (action) => {
|
|
850
|
-
landed.push(action);
|
|
851
|
-
if (!deadlineMissed) return;
|
|
852
|
-
lateLandings.push(action);
|
|
853
|
-
const before = landed.length - lateLandings.length;
|
|
854
|
-
const earlier = before > 0 ? ` (${before} other op(s) had landed before the deadline)` : "";
|
|
855
|
-
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");
|
|
856
|
-
};
|
|
857
|
-
let executed;
|
|
858
795
|
try {
|
|
859
|
-
|
|
860
|
-
} catch (
|
|
861
|
-
|
|
862
|
-
emitApplied({
|
|
863
|
-
actions: landed,
|
|
864
|
-
executionError: `execution timed out after ${config.reviewTimeoutMs}ms`
|
|
865
|
-
});
|
|
866
|
-
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)));
|
|
867
799
|
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
if (validation.rejected.length > 0) reasons.push(`${validation.rejected.length} op(s) rejected by validation`);
|
|
876
|
-
if (skippedUnread > 0) reasons.push(`${skippedUnread} op(s) skipped (skill not read this session)`);
|
|
877
|
-
if (executed.failedOps.length > 0) reasons.push(`${executed.failedOps.length} op(s) failed at execution: ${executed.failedOps.join("; ")}`);
|
|
878
|
-
if (executed.aborted !== void 0) reasons.push(`execution aborted: ${executed.aborted}`);
|
|
879
|
-
if (reasons.length === 0) reasons.push("the review plan contained nothing executable");
|
|
880
|
-
let text = `💾 Self-improvement review: 0 ops landed. ${reasons.join(" ")}`;
|
|
881
|
-
if (text.length > 500) text = `${text.slice(0, 497)}…`;
|
|
882
|
-
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)));
|
|
883
807
|
}
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
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) => {
|
|
891
830
|
try {
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
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)}`);
|
|
895
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");
|
|
896
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;
|
|
897
893
|
} catch (error) {
|
|
898
894
|
ctx.logger.warn(`dsh-evolution-review: subagent review failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
899
|
-
if (error instanceof Error && error.name === REVIEW_SETTLE_TIMEOUT) try {
|
|
900
|
-
ctx.emit("evolution/review-error", { sessionId: session.id });
|
|
901
|
-
} catch (emitError) {
|
|
902
|
-
ctx.logger.warn(`dsh-evolution-review: review-error emit failed: ${emitError instanceof Error ? emitError.message : String(emitError)}`);
|
|
903
|
-
}
|
|
904
|
-
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");
|
|
905
895
|
return false;
|
|
906
896
|
} finally {
|
|
907
897
|
reviewInFlight = false;
|
|
@@ -931,6 +921,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
931
921
|
const io = ctx.get("evolutionIo");
|
|
932
922
|
if (!io) return null;
|
|
933
923
|
return newSkillLibrary({
|
|
924
|
+
authority: authorityFrom({ origin: "foreground" }),
|
|
934
925
|
config: rootConfig,
|
|
935
926
|
io: evolutionIoAdapter(() => io.provider())
|
|
936
927
|
});
|
|
@@ -941,7 +932,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
941
932
|
let ok = true;
|
|
942
933
|
try {
|
|
943
934
|
for (const op of plan.memoryOps ?? []) {
|
|
944
|
-
if (signal?.aborted === true) {
|
|
935
|
+
if (ctx.fiber.uid === null || signal?.aborted === true) {
|
|
945
936
|
const reason = "the review was unloaded mid-plan";
|
|
946
937
|
ctx.logger.warn(`dsh-evolution-review: executePlan stopped at an op boundary after ${actions.length} op(s): ${reason}`);
|
|
947
938
|
return {
|
|
@@ -972,7 +963,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
972
963
|
}
|
|
973
964
|
}
|
|
974
965
|
for (const op of plan.skillOps ?? []) {
|
|
975
|
-
if (signal?.aborted === true) {
|
|
966
|
+
if (ctx.fiber.uid === null || signal?.aborted === true) {
|
|
976
967
|
const reason = "the review was unloaded mid-plan";
|
|
977
968
|
ctx.logger.warn(`dsh-evolution-review: executePlan stopped at an op boundary after ${actions.length} op(s): ${reason}`);
|
|
978
969
|
return {
|
|
@@ -992,17 +983,35 @@ function apply(ctx, rawConfig = {}) {
|
|
|
992
983
|
evidence: op.evidence
|
|
993
984
|
};
|
|
994
985
|
let stagedBytes;
|
|
995
|
-
if ((args.action === "update" || args.action === "edit") && hashLibrary && typeof args.name === "string" && args.name !== "")
|
|
986
|
+
if ((args.action === "update" || args.action === "edit") && hashLibrary && typeof args.name === "string" && args.name !== "") {
|
|
987
|
+
const probe = await hashLibrary.readSkillProbe(args.name);
|
|
988
|
+
if (isUnknown(probe)) {
|
|
989
|
+
ok = false;
|
|
990
|
+
failedOps.push(`skill ${args.action} ${args.name}: target is unreadable (${probe.reason}); refusing to stage an unverifiable anchor`);
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
stagedBytes = isPresent(probe) ? probe.value : null;
|
|
994
|
+
}
|
|
996
995
|
if ((args.action === "write_file" || args.action === "remove_file") && hashLibrary && typeof args.name === "string" && args.name !== "" && typeof args.file_path === "string" && args.file_path !== "") {
|
|
997
|
-
const
|
|
998
|
-
if (
|
|
996
|
+
const supportProbe = await hashLibrary.readSupportFileProbe(args.name, args.file_path);
|
|
997
|
+
if (isUnknown(supportProbe)) {
|
|
998
|
+
ok = false;
|
|
999
|
+
failedOps.push(`file ${args.action} ${args.name}/${args.file_path}: target is unreadable (${supportProbe.reason}); refusing to stage an unverifiable anchor`);
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
const stageFile = isPresent(supportProbe) ? supportProbe.value : null;
|
|
1003
|
+
args.staged_from_sha256 = stageFile === null ? "absent" : contentHash(stageFile);
|
|
999
1004
|
}
|
|
1000
1005
|
const opName = typeof args.name === "string" ? args.name : "";
|
|
1001
1006
|
const hashChecked = (args.action === "update" || args.action === "edit" || args.action === "patch") && opName !== "" && preRunHashes?.has(opName) === true;
|
|
1002
1007
|
const anchored = hashChecked && typeof stagedBytes === "string" ? stagedBytes : null;
|
|
1003
1008
|
let gateVerifiedBytes = null;
|
|
1004
1009
|
if (hashChecked) {
|
|
1005
|
-
const live = anchored !== null ? anchored :
|
|
1010
|
+
const live = anchored !== null ? anchored : await (async () => {
|
|
1011
|
+
if (hashLibrary === null) return null;
|
|
1012
|
+
const probed = await hashLibrary.readSkillProbe(opName);
|
|
1013
|
+
return isPresent(probed) ? probed.value : null;
|
|
1014
|
+
})();
|
|
1006
1015
|
const preRun = preRunHashes.get(opName);
|
|
1007
1016
|
if (live === null || preRun !== void 0 && contentHash(live) !== preRun) {
|
|
1008
1017
|
ok = false;
|
|
@@ -1022,7 +1031,8 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1022
1031
|
actions.push(`Skill ${op.name} ${op.action ?? "patch"}`);
|
|
1023
1032
|
onLanded?.(`Skill ${op.name} ${op.action ?? "patch"}`);
|
|
1024
1033
|
if (hashChecked) {
|
|
1025
|
-
const
|
|
1034
|
+
const landedProbe = hashLibrary === null ? void 0 : await hashLibrary.readSkillProbe(opName);
|
|
1035
|
+
const landed = landedProbe !== void 0 && isPresent(landedProbe) ? landedProbe.value : null;
|
|
1026
1036
|
if (landed !== null) preRunHashes.set(opName, contentHash(landed));
|
|
1027
1037
|
}
|
|
1028
1038
|
} else {
|
|
@@ -1101,6 +1111,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1101
1111
|
};
|
|
1102
1112
|
const policySnapshot = policySnapshotOf(ctx.get("evolutionPolicy"));
|
|
1103
1113
|
const library = newSkillLibrary({
|
|
1114
|
+
authority: authorityFrom({ origin: resolveOrigins(void 0, true).library }),
|
|
1104
1115
|
config: rootConfig,
|
|
1105
1116
|
io: evolutionIoAdapter(() => io.provider()),
|
|
1106
1117
|
limits: {
|
package/lib/types/index.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
30
|
-
"@lmzhen/dsh-evolution-approval": "^0.
|
|
31
|
-
"@lmzhen/dsh-evolution-core": "^0.
|
|
32
|
-
"@lmzhen/dsh-evolution-plan-validator": "^0.
|
|
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.
|
|
36
|
-
"@deepseek-ai/dsh-agent": "^0.2.
|
|
37
|
-
"@deepseek-ai/dsh-llm": "^0.2.
|
|
38
|
-
"@deepseek-ai/dsh-session": "^0.2.
|
|
39
|
-
"@deepseek-ai/dsh-tools": "^0.2.
|
|
40
|
-
"@lmzhen/dsh-evolution-
|
|
41
|
-
"@lmzhen/dsh-evolution-
|
|
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.
|
|
45
|
-
"@deepseek-ai/dsh-agent-loop-testkit": "^0.2.
|
|
46
|
-
"@deepseek-ai/dsh-llm": "^0.2.
|
|
47
|
-
"@deepseek-ai/dsh-session": "^0.2.
|
|
48
|
-
"@deepseek-ai/dsh-session-persistence": "^0.2.
|
|
49
|
-
"@deepseek-ai/dsh-session-persistence-jsonl": "^0.2.
|
|
50
|
-
"@deepseek-ai/dsh-tools": "^0.2.
|
|
51
|
-
"@lmzhen/dsh-evolution-approval": "^0.
|
|
52
|
-
"@lmzhen/dsh-evolution-core": "^0.
|
|
53
|
-
"@lmzhen/dsh-evolution-curator": "^0.
|
|
54
|
-
"@lmzhen/dsh-evolution-plan-validator": "^0.
|
|
55
|
-
"@lmzhen/dsh-evolution-state": "^0.
|
|
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
|
}
|