@lmzhen/dsh-evolution-review 0.30.28 → 0.30.30

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,9 +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, ensureSkillFrontmatter, 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
- import { createHash, randomUUID } from "node:crypto";
3
1
  import z from "@deepseek-ai/schemastery";
2
+ import { randomUUID } from "node:crypto";
4
3
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
5
- import { SessionId } from "@deepseek-ai/dsh-session";
6
- import { planContractPrompt, validateEvolutionPlan } from "@lmzhen/dsh-evolution-plan-validator";
4
+ import { SessionId, SessionLogOffset } from "@deepseek-ai/dsh-session";
5
+ import { COMPLETION_SKILL_REVIEW_PROMPT, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_REVIEW_TIMEOUT_MS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, MAX_TIMER_DELAY_MS, PROMPT_BUNDLE, advanceReview, assertSkillsRootAliasRetired, clampOnce, classifySessionEvent, clearReviewChannel, filterUnreadSkillOps, liveBool, markReviewChannel, paramRowId, pickWithPolicy, readDispatchSignal, reviewPrompt, sessionAudited, sessionTurnSignals, sweepReviewChannelSessions, unwrapLive, userSetKeys, verifyPromptBundle } from "@lmzhen/dsh-evolution-core";
7
6
  /**
8
7
  * Whether a message is a notice this plugin delivered, judged only by durable source fields.
9
8
  *
@@ -149,17 +148,12 @@ function noticeAllowed(reviewPrompt, notice) {
149
148
  }
150
149
  const Config = z.object({
151
150
  reviewEnabled: z.boolean().default(true).volatile(),
152
- reviewMode: z.union([z.const("subagent"), z.const("inject")]).default("inject").volatile(),
151
+ reviewMode: z.union([z.const("inject"), z.const("branch")]).default("inject").volatile(),
153
152
  reviewMemoryInterval: z.number().min(1).volatile(),
154
153
  reviewSkillInterval: z.number().min(1).volatile(),
155
154
  memoryInterval: z.number().min(1).default(DEFAULT_REVIEW_MEMORY_INTERVAL),
156
155
  skillInterval: z.number().min(1).default(DEFAULT_REVIEW_SKILL_INTERVAL),
157
- reviewToolAllow: z.array(z.string()).default(["skill"]),
158
156
  reviewTimeoutMs: z.number().min(1).max(MAX_TIMER_DELAY_MS).default(DEFAULT_REVIEW_TIMEOUT_MS),
159
- reviewContextMessages: z.number().min(1).default(DEFAULT_REVIEW_CONTEXT_MESSAGES),
160
- reviewMessageChars: z.number().min(1).default(DEFAULT_REVIEW_MESSAGE_CHARS),
161
- reviewMaxDepth: z.number().min(1).default(1),
162
- reviewProvider: z.string(),
163
157
  skillReviewTrigger: z.union([
164
158
  z.const("cadence"),
165
159
  z.const("completion"),
@@ -172,24 +166,6 @@ const Config = z.object({
172
166
  root: z.string().default(""),
173
167
  skillsRoot: z.string().default("")
174
168
  });
175
- /** V8-01 (0.3.45): the review subagent's structured-output contract. The
176
- * `subagents.start` outputSchema is RAW JSON Schema validated by
177
- * assertObjectJsonSchema (upstream dsh-tools json-schema.ts — SCHEMA_TYPES
178
- * has no 'json'; 'json' is a defineTool-author DSL-only value). The arrays
179
- * deliberately use `{ type: 'array' }` with NO items node — absent items
180
- * accepts any JSON item (upstream semantics, json-schema.ts "Item schema
181
- * (type: 'array' only); absent accepts any JSON item"), and plan-validator
182
- * owns the per-op structure proof. The whitelist contract test (review.spec
183
- * V8-01) forbids a DSL value slipping back in. */
184
- const REVIEW_OUTPUT_SCHEMA = {
185
- type: "object",
186
- additionalProperties: false,
187
- properties: {
188
- memoryOps: { type: "array" },
189
- skillOps: { type: "array" },
190
- summary: { type: "string" }
191
- }
192
- };
193
169
  /**
194
170
  * v43 audit (P1-4 / FLOW1): the cadence summary MUST name the review kind — it
195
171
  * is what tells the three cadence deliveries apart (their prompts differ by
@@ -211,15 +187,7 @@ function clampReviewConfig(rawConfig, ctx) {
211
187
  ctx.logger.warn("dsh-evolution-review: " + message);
212
188
  };
213
189
  const field = (name, value, fallback, min, max) => clampOnce(warned, warn, name, value, fallback, min, max);
214
- return Object.assign({}, rawConfig, {
215
- reviewTimeoutMs: field("reviewTimeoutMs", rawConfig.reviewTimeoutMs, DEFAULT_REVIEW_TIMEOUT_MS, 1, MAX_TIMER_DELAY_MS),
216
- reviewContextMessages: field("reviewContextMessages", rawConfig.reviewContextMessages, DEFAULT_REVIEW_CONTEXT_MESSAGES, 1),
217
- reviewMessageChars: field("reviewMessageChars", rawConfig.reviewMessageChars, DEFAULT_REVIEW_MESSAGE_CHARS, 1),
218
- reviewMaxDepth: field("reviewMaxDepth", rawConfig.reviewMaxDepth, 1, 1)
219
- });
220
- }
221
- function policySnapshotOf(source) {
222
- return source?.get?.();
190
+ return Object.assign({}, rawConfig, { reviewTimeoutMs: field("reviewTimeoutMs", rawConfig.reviewTimeoutMs, DEFAULT_REVIEW_TIMEOUT_MS, 1, MAX_TIMER_DELAY_MS) });
223
191
  }
224
192
  /**
225
193
  * This row's review knobs as they stand RIGHT NOW (G1 §8.3) — the base layer of the
@@ -253,9 +221,7 @@ function apply(ctx, rawConfig = {}) {
253
221
  });
254
222
  if (!verifyPromptBundle(PROMPT_BUNDLE)) throw new Error("dsh-evolution prompt bundle integrity check failed; refusing to schedule review work");
255
223
  const config = clampReviewConfig(rawConfig, ctx);
256
- const reviewChannel = createSubagentChannel(ctx);
257
224
  assertSkillsRootAliasRetired(rawConfig);
258
- const rootConfig = resolveRootConfig(rawConfig);
259
225
  const sessionState = new SessionScopedState();
260
226
  const dispatchDigest = sessionState.add("dispatchDigest", /* @__PURE__ */ new Map());
261
227
  let statelessReviewStateWarned = false;
@@ -267,9 +233,10 @@ function apply(ctx, rawConfig = {}) {
267
233
  const pendingCadenceWarned = sessionState.add("pendingCadenceWarned", /* @__PURE__ */ new Set());
268
234
  const pendingReviewNotices = sessionState.add("pendingReviewNotices", /* @__PURE__ */ new Map());
269
235
  const cadenceResetWarned = sessionState.add("cadenceResetWarned", /* @__PURE__ */ new Set());
270
- let reviewInFlight = false;
271
- /** W5 G3-S3: capability-missing is warned once per plugin instance (the error signal is per trigger). */
272
- let channelUnavailableWarned = false;
236
+ /** Branch mode runs at most one branch per parent session; a second trigger only sets rerun. */
237
+ const branchRunning = sessionState.add("branchRunning", /* @__PURE__ */ new Map());
238
+ /** Child Sessions this plugin owns; their turn/end must not start another review. */
239
+ const branchSessions = sessionState.add("branchSessions", /* @__PURE__ */ new Set());
273
240
  const policy = () => ctx.get("evolutionPolicy")?.get();
274
241
  /**
275
242
  * This row's knobs as they stand right now (G1 §8.3): the live fields are resolved
@@ -435,6 +402,8 @@ function apply(ctx, rawConfig = {}) {
435
402
  async function runOnTurnEnd(session, event) {
436
403
  if (!params().reviewEnabled) return;
437
404
  if (session.header.origin === "subagent") return;
405
+ if (branchSessions.has(session.id)) return;
406
+ if (session.header.visibility === "background") return;
438
407
  const agent = ctx.agents.get(session.id);
439
408
  if (!agent) return;
440
409
  const signal = sessionTurnSignals(ctx, session);
@@ -480,7 +449,8 @@ function apply(ctx, rawConfig = {}) {
480
449
  const pendingKind = kind ?? pendingCadenceReviews.get(session.id) ?? void 0;
481
450
  if (pendingKind !== void 0) {
482
451
  pendingCadenceReviews.delete(session.id);
483
- if (params().reviewMode === "inject") {
452
+ if (params().reviewMode === "branch" && await tryBranchReview(session, agent, pendingKind, signal, "cadence")) return;
453
+ if (params().reviewMode === "inject" || params().reviewMode === "branch") {
484
454
  if (!deliverMessage(agent, reviewPrompt(pendingKind), cadenceSummary(pendingKind), true)) {
485
455
  pendingCadenceReviews.set(session.id, pendingKind);
486
456
  return;
@@ -493,35 +463,6 @@ function apply(ctx, rawConfig = {}) {
493
463
  assistantChars: signal.assistantChars,
494
464
  channel: "inject"
495
465
  });
496
- } else {
497
- const reviewOutcome = await trySubagentReview(session, agent, pendingKind, signal);
498
- if (reviewOutcome === true) {
499
- emitReviewScheduled(ctx, {
500
- sessionId: session.id,
501
- kind: pendingKind,
502
- toolCalls: signal.toolCalls,
503
- userChars: signal.userChars,
504
- assistantChars: signal.assistantChars,
505
- channel: "subagent"
506
- });
507
- await settleNotice(session.id, "subagent-flush");
508
- } else if (reviewOutcome === "dropped") {
509
- pendingCadenceReviews.set(session.id, pendingKind);
510
- return;
511
- } else if (reviewOutcome !== "deferred") {
512
- if (!deliverMessage(agent, reviewPrompt(pendingKind), cadenceSummary(pendingKind), true)) {
513
- pendingCadenceReviews.set(session.id, pendingKind);
514
- return;
515
- }
516
- emitReviewScheduled(ctx, {
517
- sessionId: session.id,
518
- kind: pendingKind,
519
- toolCalls: signal.toolCalls,
520
- userChars: signal.userChars,
521
- assistantChars: signal.assistantChars,
522
- channel: "inject"
523
- });
524
- }
525
466
  }
526
467
  return;
527
468
  }
@@ -540,22 +481,7 @@ function apply(ctx, rawConfig = {}) {
540
481
  if (completionInjected.has(session.id)) return;
541
482
  if (!shouldCompletionReview(event.data.reason, cumulative, params().skillReviewCompletionMinToolCalls)) return;
542
483
  completionInjected.add(session.id);
543
- if (reviewInFlight) {
544
- if (deferredFallbackReviews.length < DEFERRED_REVIEW_CAP) deferredFallbackReviews.push({
545
- agent,
546
- sessionId: session.id,
547
- kind: "skill",
548
- prompt: COMPLETION_SKILL_REVIEW_PROMPT,
549
- label: "completion review",
550
- channel: "completion",
551
- counts: signal
552
- });
553
- else {
554
- completionInjected.delete(session.id);
555
- ctx.logger.warn(`dsh-evolution-review: deferred-review queue at cap (${DEFERRED_REVIEW_CAP}) — dropped one deferred completion review prompt; the completion gate re-arms for the next completed turn`);
556
- }
557
- return;
558
- }
484
+ if (params().reviewMode === "branch" && await tryBranchReview(session, agent, "skill", signal, "completion")) return;
559
485
  if (!deliverMessage(agent, COMPLETION_SKILL_REVIEW_PROMPT, "completion review", true)) {
560
486
  completionInjected.delete(session.id);
561
487
  return;
@@ -679,527 +605,141 @@ function apply(ctx, rawConfig = {}) {
679
605
  return false;
680
606
  }
681
607
  };
682
- /**
683
- * T3-09/A37: the ONE abort channel for this fiber's long work.
684
- *
685
- * The review's two legs (the review subagent, and the write leg that executes its plan) were
686
- * bounded only by their own timeouts, so an unloaded or HMR-replaced fiber kept running — and the
687
- * write leg kept WRITING — while the replacement instance knew nothing about it (its single-flight
688
- * flag and min-idle window do not cover a run that no longer belongs to any live fiber). The
689
- * disposer below aborts this controller; the subagent leg merges it with its own deadline through
690
- * `AbortSignal.any` (the shape evolution-maintenance's scan already uses) and the write leg checks
691
- * it at every op boundary — before the first op, so an aborted fiber lands nothing.
692
- */
693
- const fiberAbort = new AbortController();
694
- const withTimeout = (promise, ms, label) => new Promise((resolve, reject) => {
608
+ const BRANCH_MEMORY_PROMPT = "[Auto-review — Memory] Review the conversation above and save durable user preferences, corrections, or stable facts with the memory tool when appropriate. Do not create skills here. Return a short summary of what was saved, or say Nothing to save.";
609
+ async function tryBranchReview(session, agent, kind, signal, reason) {
610
+ const state = branchRunning.get(session.id);
611
+ if (state !== void 0) {
612
+ state.rerun = kind;
613
+ return true;
614
+ }
615
+ const branchState = {};
616
+ branchRunning.set(session.id, branchState);
617
+ const childId = SessionId("session-" + randomUUID());
618
+ const startedAt = Date.now();
619
+ const controller = new AbortController();
620
+ let timedOut = false;
695
621
  const timer = setTimeout(() => {
696
- reject(/* @__PURE__ */ new Error(`dsh-evolution-review: ${label} timed out after ${ms}ms`));
697
- }, ms);
698
- promise.then((value) => {
699
- clearTimeout(timer);
700
- resolve(value);
701
- }, (error) => {
702
- clearTimeout(timer);
703
- reject(error instanceof Error ? error : new Error(String(error)));
704
- });
705
- });
706
- const deferredFallbackReviews = [];
707
- const DEFERRED_REVIEW_CAP = 16;
708
- ctx.effect(() => () => {
709
- deferredFallbackReviews.length = 0;
710
- }, "dsh-evolution-review.deferred-drain");
711
- /** v32 REV-06(b): content hash of every tree skill at REVIEW SCHEDULE time.
712
- * The subagent's read-time hash is not plumbed through the plan, so this
713
- * pre-run snapshot is what the execute-time staleness check can honestly
714
- * compare against: a skill whose hash changed while the review ran was
715
- * touched concurrently, and the plan (authored against the old state) is
716
- * refused as stale. */
717
- async function treeSkillHashes() {
718
- const hashes = /* @__PURE__ */ new Map();
719
- const io = ctx.get("evolutionIo");
720
- if (!io) return hashes;
721
- const library = newSkillLibrary({
722
- authority: authorityFrom({ origin: "foreground" }),
723
- config: rootConfig,
724
- io: evolutionIoAdapter(() => io.provider())
725
- });
726
- for (const summary of await library.list({ withContent: true }).catch((error) => {
727
- 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`);
728
- return [];
729
- })) if (summary.content !== void 0) hashes.set(summary.name, contentHash(summary.content));
730
- return hashes;
731
- }
732
- async function trySubagentReview(session, agent, kind, signal) {
733
- if (params().reviewMode === "inject") return false;
734
- const channelState = reviewChannel.state;
735
- if (channelState.kind !== "available") {
736
- if (!channelUnavailableWarned) {
737
- channelUnavailableWarned = true;
738
- ctx.logger.warn("dsh-evolution-review: subagent channel unavailable (" + channelState.code + ": " + channelState.reason + ") — reviews fall back to the inject path until the capability returns");
739
- }
740
- try {
741
- ctx.emit("evolution/review-error", { sessionId: session.id });
742
- } catch (emitError) {
743
- ctx.logger.warn("dsh-evolution-review: review-error emit failed: " + (emitError instanceof Error ? emitError.message : String(emitError)));
622
+ timedOut = true;
623
+ controller.abort();
624
+ }, rawConfig.reviewTimeoutMs ?? DEFAULT_REVIEW_TIMEOUT_MS);
625
+ let handle;
626
+ try {
627
+ const presets = ctx.get("agentPresets");
628
+ let agentPreset;
629
+ let setup;
630
+ if (presets !== void 0) {
631
+ const resolved = await presets.resolve(session.header.agentPreset);
632
+ agentPreset = resolved.id;
633
+ setup = async (target, _child) => {
634
+ await presets.mount(target, resolved.id);
635
+ };
744
636
  }
637
+ const seed = session.snapshotEvents();
638
+ const current = ctx.get("agentDefaultModel")?.currentSelection?.();
639
+ handle = await ctx.agents.create({
640
+ sessionId: childId,
641
+ parentAgent: agent,
642
+ seed,
643
+ inheritedEventCount: SessionLogOffset(seed.length),
644
+ meta: {
645
+ parentSession: session.id,
646
+ isSeeded: true,
647
+ ...agentPreset === void 0 ? {} : { agentPreset }
648
+ },
649
+ ...current === void 0 ? {} : { agentOptions: {
650
+ provider: current.provider,
651
+ model: current.model
652
+ } },
653
+ ...setup === void 0 ? {} : { setup }
654
+ });
655
+ } catch (error) {
656
+ clearTimeout(timer);
657
+ branchRunning.delete(session.id);
658
+ ctx.logger.warn("dsh-evolution-review: branch Session did not start: " + (error instanceof Error ? error.message : String(error)));
745
659
  return false;
746
660
  }
747
- if (reviewInFlight) {
748
- const existing = deferredFallbackReviews.findIndex((entry) => entry.sessionId === session.id && entry.kind === kind);
749
- if (existing >= 0 || deferredFallbackReviews.length < DEFERRED_REVIEW_CAP) {
750
- const entry = {
751
- agent,
752
- sessionId: session.id,
753
- kind,
754
- prompt: reviewPrompt(kind),
755
- label: cadenceSummary(kind),
756
- channel: "inject",
757
- counts: signal
661
+ branchSessions.add(childId);
662
+ const collected = {
663
+ finalText: "",
664
+ actions: [],
665
+ usage: void 0,
666
+ error: void 0
667
+ };
668
+ const onEvent = (owner, event) => {
669
+ if (owner.id !== childId) return;
670
+ if (event.type === "assistant/message") {
671
+ const text = event.data.message.content.filter((block) => block.type === "text").map((block) => block.text).join("");
672
+ if (text !== "") collected.finalText = text;
673
+ if (event.data.usage !== void 0) collected.usage = {
674
+ inputTokens: event.data.usage.inputTokens ?? 0,
675
+ outputTokens: event.data.usage.outputTokens ?? 0,
676
+ cacheReadTokens: event.data.usage.cacheReadTokens ?? 0,
677
+ cacheWriteTokens: event.data.usage.cacheWriteTokens ?? 0
758
678
  };
759
- if (existing >= 0) deferredFallbackReviews[existing] = entry;
760
- else deferredFallbackReviews.push(entry);
761
- return "deferred";
762
- }
763
- ctx.logger.warn(`dsh-evolution-review: deferred-review queue at cap (${DEFERRED_REVIEW_CAP}) — dropping one fallback review prompt`);
764
- return "dropped";
765
- }
766
- reviewInFlight = true;
767
- try {
768
- const snapshot = policy();
769
- const model = kind === "memory" ? snapshot?.memoryReviewModel ?? DEFAULT_MEMORY_REVIEW_MODEL : snapshot?.skillReviewModel ?? DEFAULT_SKILL_REVIEW_MODEL;
770
- const reviewText = redactSecrets(buildReviewRequest(session, kind, signal, config.reviewContextMessages, config.reviewMessageChars, dispatchDigest.get(session.id) ?? []));
771
- const agentOptions = { model };
772
- if (config.reviewProvider) agentOptions.provider = config.reviewProvider;
773
- const requestedReviewTools = config.reviewToolAllow ?? [];
774
- const preRunHashes = await treeSkillHashes();
775
- const sessionSeqAtPlanTime = session.seq - 1;
776
- const substantiveEvidenceSeqs = sessionEvidenceIndex(ctx, session);
777
- const outcome = await reviewChannel.run({
778
- parent: agent,
779
- label: "dsh-evolution-review",
780
- promptText: reviewText + "\n\n" + reviewPrompt(kind, "plan"),
781
- kind: "reviewer",
782
- allow: requestedReviewTools,
783
- outputSchema: REVIEW_OUTPUT_SCHEMA,
784
- maxDepth: config.reviewMaxDepth,
785
- agentOptions,
786
- timeoutMs: config.reviewTimeoutMs,
787
- signal: fiberAbort.signal,
788
- afterResult: (child) => {
789
- const childSession = child.session;
790
- return childSession === void 0 ? /* @__PURE__ */ new Set() : new Set(sessionReadNames(ctx, childSession) ?? []);
791
- }
792
- });
793
- if (outcome.kind === "aborted") {
794
- try {
795
- ctx.emit("evolution/review-error", { sessionId: session.id });
796
- } catch (emitError) {
797
- ctx.logger.warn("dsh-evolution-review: review-error emit failed: " + (emitError instanceof Error ? emitError.message : String(emitError)));
798
- }
799
- return false;
800
- }
801
- if (outcome.kind === "unavailable") {
802
- if (!channelUnavailableWarned) {
803
- channelUnavailableWarned = true;
804
- ctx.logger.warn("dsh-evolution-review: subagent channel unavailable (" + outcome.code + ": " + outcome.reason + ") — reviews fall back to the inject path until the capability returns");
805
- }
806
- try {
807
- ctx.emit("evolution/review-error", { sessionId: session.id });
808
- } catch (emitError) {
809
- ctx.logger.warn("dsh-evolution-review: review-error emit failed: " + (emitError instanceof Error ? emitError.message : String(emitError)));
810
- }
811
- return false;
812
- }
813
- if (outcome.kind === "no-structured") {
814
- try {
815
- ctx.emit("evolution/review-error", { sessionId: session.id });
816
- } catch (emitError) {
817
- ctx.logger.warn("dsh-evolution-review: review-error emit failed: " + (emitError instanceof Error ? emitError.message : String(emitError)));
818
- }
819
- const stopDetail = typeof outcome.stopReason === "string" ? outcome.stopReason : void 0;
820
- const diagDetail = typeof outcome.diagnostic === "string" ? outcome.diagnostic : void 0;
821
- ctx.logger.warn("dsh-evolution-review: review subagent returned no structured plan" + (stopDetail !== void 0 ? " (stopReason=" + stopDetail + (diagDetail !== void 0 ? "; " + diagDetail : "") + ")" : ""));
822
- return false;
823
679
  }
824
- const childReads = outcome.evidence ?? /* @__PURE__ */ new Set();
825
- const plan = outcome.structured;
826
- const policyFingerprint = fingerprintPolicy(snapshot);
827
- const validation = validateEvolutionPlan(plan, {
828
- sessionSeq: sessionSeqAtPlanTime,
829
- substantiveEvidenceSeqs,
830
- maxOpsPerPlan: snapshot?.maxOpsPerPlan ?? DEFAULT_MAX_OPS_PER_PLAN,
831
- protectedSkillNames: new Set(snapshot?.protectedSkillNames ?? []),
832
- maxMemoryChars: snapshot?.memoryChars ?? DEFAULT_MEMORY_CHAR_LIMIT,
833
- maxUserChars: snapshot?.userChars ?? DEFAULT_USER_CHAR_LIMIT,
834
- maxSkillContentChars: snapshot?.skillContentChars ?? DEFAULT_SKILL_CONTENT_CHARS
835
- });
836
- 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("; ")}`);
837
- const acceptedSkillOps = validation.accepted.skillOps ?? [];
838
- const skippedUnread = filterUnreadSkillOps$1(acceptedSkillOps, new Set([...sessionReadNames(ctx, session) ?? /* @__PURE__ */ new Set(), ...childReads]));
839
- const evidenceQuotes = [...validation.accepted.memoryOps ?? [], ...acceptedSkillOps].reduce((total, op) => total + (Array.isArray(op.evidence) ? op.evidence.length : 0), 0);
840
- const emitApplied = (report) => {
680
+ };
681
+ const offEvent = ctx.on("session/event", onEvent);
682
+ emitReviewScheduled(ctx, {
683
+ sessionId: session.id,
684
+ kind,
685
+ toolCalls: signal.toolCalls,
686
+ userChars: signal.userChars,
687
+ assistantChars: signal.assistantChars,
688
+ channel: "branch"
689
+ });
690
+ (async () => {
691
+ try {
692
+ const prompt = kind === "memory" ? BRANCH_MEMORY_PROMPT : COMPLETION_SKILL_REVIEW_PROMPT;
693
+ handle.agent.followup(createUserMessage({
694
+ content: [{
695
+ type: "text",
696
+ text: prompt
697
+ }],
698
+ source: { kind: "user" }
699
+ }));
700
+ await handle.agent.whenIdle();
701
+ offEvent();
702
+ if (collected.finalText.trim() !== "") deliverMessage(agent, collected.finalText, "branch review result", false);
841
703
  try {
842
- ctx.emit("evolution/plan-applied", {
704
+ ctx.emit("evolution/review-completed", {
705
+ version: 1,
706
+ reviewId: childId,
843
707
  sessionId: session.id,
844
- planId: randomUUID(),
845
- policyFingerprint,
846
- memoryApplied: report.actions.filter((action) => action.startsWith("Memory")).length,
847
- skillApplied: report.actions.filter((action) => action.startsWith("Skill ")).length,
848
- rejectedOps: validation.rejected.length,
849
- ...skippedUnread > 0 ? { skippedUnread } : {},
850
- ...validation.reports.length > 0 ? { evidenceClassReports: validation.reports.length } : {},
851
- executionFailures: report.failedOps?.length ?? 0,
852
- ...report.executionError !== void 0 ? { executionError: report.executionError } : {},
853
- evidenceQuotes,
854
- estimatedInputChars: reviewText.length
708
+ branchSessionId: childId,
709
+ kind,
710
+ status: collected.error !== void 0 ? "failed" : timedOut ? "timeout" : controller.signal.aborted ? "aborted" : "completed",
711
+ trigger: {
712
+ reason,
713
+ toolCalls: signal.toolCalls,
714
+ userChars: signal.userChars,
715
+ assistantChars: signal.assistantChars
716
+ },
717
+ finalText: collected.finalText,
718
+ actions: collected.actions,
719
+ ...collected.usage === void 0 ? {} : { usage: collected.usage },
720
+ startedAt,
721
+ finishedAt: Date.now(),
722
+ ...collected.error === void 0 ? {} : { error: collected.error }
855
723
  });
856
724
  } catch (emitError) {
857
- ctx.logger.warn(`dsh-evolution-review: plan-applied emit failed: ${emitError instanceof Error ? emitError.message : String(emitError)}`);
725
+ ctx.logger.warn("dsh-evolution-review: review-completed emit failed: " + (emitError instanceof Error ? emitError.message : String(emitError)));
858
726
  }
859
- };
860
- const landed = [];
861
- let deadlineMissed = false;
862
- const lateLandings = [];
863
- const noteLanded = (action) => {
864
- landed.push(action);
865
- if (!deadlineMissed) return;
866
- lateLandings.push(action);
867
- const before = landed.length - lateLandings.length;
868
- const earlier = before > 0 ? ` (${before} other op(s) had landed before the deadline)` : "";
869
- 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");
870
- };
871
- let executed;
872
- try {
873
- executed = await withTimeout(executePlan(validation.accepted, session, noteLanded, preRunHashes, fiberAbort.signal), config.reviewTimeoutMs, "review plan execution");
874
727
  } catch (error) {
875
- deadlineMissed = true;
876
- emitApplied({
877
- actions: landed,
878
- executionError: `execution timed out after ${config.reviewTimeoutMs}ms`
879
- });
880
- throw error;
881
- }
882
- const actions = executed.actions;
883
- if (actions.length > 0) {
884
- const applied = actions.join(" · ");
885
- const failedNote = executed.failedOps.length > 0 ? ` 失败 ${executed.failedOps.length} 个:${executed.failedOps.join(";")}。` : "";
886
- deliverMessage(agent, `💾 Self-improvement review: ${applied}${executed.ok ? "" : `\n部分操作失败。${failedNote}以上操作已应用,请勿重复执行。`}`, "self-improvement review");
887
- } else {
888
- const reasons = [];
889
- if (validation.rejected.length > 0) reasons.push(`${validation.rejected.length} op(s) rejected by validation`);
890
- if (skippedUnread > 0) reasons.push(`${skippedUnread} op(s) skipped (skill not read this session)`);
891
- if (executed.failedOps.length > 0) reasons.push(`${executed.failedOps.length} op(s) failed at execution: ${executed.failedOps.join("; ")}`);
892
- if (executed.aborted !== void 0) reasons.push(`execution aborted: ${executed.aborted}`);
893
- if (reasons.length === 0) reasons.push("the review plan contained nothing executable");
894
- let text = `💾 Self-improvement review: 0 ops landed. ${reasons.join(" ")}`;
895
- if (text.length > 500) text = `${text.slice(0, 497)}…`;
896
- deliverMessage(agent, text, "self-improvement review");
897
- }
898
- emitApplied({
899
- actions,
900
- failedOps: executed.failedOps,
901
- ...executed.aborted !== void 0 ? { executionError: executed.aborted } : executed.failedOps[0] !== void 0 ? { executionError: executed.failedOps[0] } : {}
902
- });
903
- return true;
904
- } catch (error) {
905
- ctx.logger.warn(`dsh-evolution-review: subagent review failed: ${error instanceof Error ? error.message : String(error)}`);
906
- return false;
907
- } finally {
908
- reviewInFlight = false;
909
- const deferred = deferredFallbackReviews.splice(0);
910
- for (const { agent: waitingAgent, sessionId: entrySession, kind: waitingKind, prompt, label, channel, counts: entryCounts } of deferred) {
911
- if (!deliverMessage(waitingAgent, prompt, label, true)) {
912
- if (channel === "completion") completionInjected.delete(entrySession);
913
- else pendingCadenceReviews.set(entrySession, waitingKind);
914
- continue;
915
- }
916
- emitReviewScheduled(ctx, {
917
- sessionId: entrySession,
918
- kind: waitingKind,
919
- toolCalls: entryCounts.toolCalls,
920
- userChars: entryCounts.userChars,
921
- assistantChars: entryCounts.assistantChars,
922
- channel
923
- });
728
+ offEvent();
729
+ collected.error = error instanceof Error ? error.message : String(error);
730
+ ctx.logger.warn("dsh-evolution-review: branch review failed: " + collected.error);
731
+ } finally {
732
+ clearTimeout(timer);
733
+ branchSessions.delete(childId);
734
+ await handle.dispose().catch(() => {});
735
+ const rerun = branchState.rerun;
736
+ branchRunning.delete(session.id);
737
+ if (rerun !== void 0) tryBranchReview(session, agent, rerun, signal, reason);
924
738
  }
925
- }
926
- }
927
- async function executePlan(plan, session, onLanded, preRunHashes, signal) {
928
- const sessionId = session?.id;
929
- const memory = ctx.get("memory");
930
- const approval = ctx.get("evolutionApproval");
931
- const hashLibrary = (() => {
932
- const io = ctx.get("evolutionIo");
933
- if (!io) return null;
934
- return newSkillLibrary({
935
- authority: authorityFrom({ origin: "foreground" }),
936
- config: rootConfig,
937
- io: evolutionIoAdapter(() => io.provider())
938
- });
939
739
  })();
940
- const origins = resolveOrigins(void 0, true);
941
- const actions = [];
942
- const failedOps = [];
943
- let ok = true;
944
- try {
945
- for (const op of plan.memoryOps ?? []) {
946
- if (ctx.fiber.uid === null || signal?.aborted === true) {
947
- const reason = "the review was unloaded mid-plan";
948
- ctx.logger.warn(`dsh-evolution-review: executePlan stopped at an op boundary after ${actions.length} op(s): ${reason}`);
949
- return {
950
- actions,
951
- ok: false,
952
- failedOps,
953
- aborted: reason
954
- };
955
- }
956
- if (!Array.isArray(op.evidence) || op.evidence.length === 0) {
957
- ok = false;
958
- failedOps.push(`memory ${op.action ?? "add"} ${op.target}: missing evidence (defense-in-depth rejection)`);
959
- continue;
960
- }
961
- const normalized = {
962
- target: op.target === "user" ? "user" : "memory",
963
- action: op.action ?? "add",
964
- facts: payloadText(op),
965
- old_text: op.old_text
966
- };
967
- const result = approval ? await runApproved("memory", `memory ${normalized.target} ${normalized.action}`, normalized, normalized, session) : await memory?.applyBatch(normalized.target, [normalized]);
968
- if (result?.ok) {
969
- actions.push("Memory updated");
970
- onLanded?.("Memory updated");
971
- } else {
972
- ok = false;
973
- failedOps.push(`memory ${normalized.action} ${normalized.target}: ${result?.message ?? "service unavailable"}`);
974
- }
975
- }
976
- for (const op of plan.skillOps ?? []) {
977
- if (ctx.fiber.uid === null || signal?.aborted === true) {
978
- const reason = "the review was unloaded mid-plan";
979
- ctx.logger.warn(`dsh-evolution-review: executePlan stopped at an op boundary after ${actions.length} op(s): ${reason}`);
980
- return {
981
- actions,
982
- ok: false,
983
- failedOps,
984
- aborted: reason
985
- };
986
- }
987
- if (!Array.isArray(op.evidence) || op.evidence.length === 0 || !op.name) {
988
- ok = false;
989
- failedOps.push(`skill ${op.action ?? "patch"} ${op.name ?? "<unnamed>"}: missing evidence or name (defense-in-depth rejection)`);
990
- continue;
991
- }
992
- const args = {
993
- ...op,
994
- evidence: op.evidence
995
- };
996
- if ((args.action === "create" || args.action === "edit" || args.action === "update") && typeof args.content === "string" && typeof args.name === "string") {
997
- const repaired = ensureSkillFrontmatter(args.content, args.name, args.description);
998
- if (repaired.repaired) {
999
- args.content = repaired.content;
1000
- ctx.logger.warn(`dsh-evolution-review: synthesized missing SKILL.md frontmatter for ${args.name}`);
1001
- }
1002
- }
1003
- let stagedBytes;
1004
- if ((args.action === "update" || args.action === "edit") && hashLibrary && typeof args.name === "string" && args.name !== "") {
1005
- const probe = await hashLibrary.readSkillProbe(args.name);
1006
- if (isUnknown(probe)) {
1007
- ok = false;
1008
- failedOps.push(`skill ${args.action} ${args.name}: target is unreadable (${probe.reason}); refusing to stage an unverifiable anchor`);
1009
- continue;
1010
- }
1011
- stagedBytes = isPresent(probe) ? probe.value : null;
1012
- }
1013
- if ((args.action === "write_file" || args.action === "remove_file") && hashLibrary && typeof args.name === "string" && args.name !== "" && typeof args.file_path === "string" && args.file_path !== "") {
1014
- const supportProbe = await hashLibrary.readSupportFileProbe(args.name, args.file_path);
1015
- if (isUnknown(supportProbe)) {
1016
- ok = false;
1017
- failedOps.push(`file ${args.action} ${args.name}/${args.file_path}: target is unreadable (${supportProbe.reason}); refusing to stage an unverifiable anchor`);
1018
- continue;
1019
- }
1020
- const stageFile = isPresent(supportProbe) ? supportProbe.value : null;
1021
- args.staged_from_sha256 = stageFile === null ? "absent" : contentHash(stageFile);
1022
- }
1023
- const opName = typeof args.name === "string" ? args.name : "";
1024
- const hashChecked = (args.action === "update" || args.action === "edit" || args.action === "patch") && opName !== "" && preRunHashes?.has(opName) === true;
1025
- const anchored = hashChecked && typeof stagedBytes === "string" ? stagedBytes : null;
1026
- let gateVerifiedBytes = null;
1027
- if (hashChecked) {
1028
- const live = anchored !== null ? anchored : await (async () => {
1029
- if (hashLibrary === null) return null;
1030
- const probed = await hashLibrary.readSkillProbe(opName);
1031
- return isPresent(probed) ? probed.value : null;
1032
- })();
1033
- const preRun = preRunHashes.get(opName);
1034
- if (live === null || preRun !== void 0 && contentHash(live) !== preRun) {
1035
- ok = false;
1036
- failedOps.push(`skill ${args.action} ${args.name}: the skill changed while this review ran — ${args.action === "patch" ? "patch" : "update"} refused as stale; produce a fresh plan`);
1037
- continue;
1038
- }
1039
- if (anchored === null) gateVerifiedBytes = live;
1040
- }
1041
- const anchorBytes = anchored ?? gateVerifiedBytes ?? stagedBytes;
1042
- if ((args.action === "update" || args.action === "edit") && anchorBytes !== void 0) args.staged_from_sha256 = anchorBytes === null ? "absent" : contentHash(anchorBytes);
1043
- const runnerArgs = {
1044
- operation: args,
1045
- origin: origins.library
1046
- };
1047
- const result = approval ? await runApproved("skill", `skill ${op.action ?? "patch"} ${op.name}`, runnerArgs, runnerArgs, session) : await executeSkillDirect(args);
1048
- if (result?.ok) {
1049
- actions.push(`Skill ${op.name} ${op.action ?? "patch"}`);
1050
- onLanded?.(`Skill ${op.name} ${op.action ?? "patch"}`);
1051
- if (hashChecked) {
1052
- const landedProbe = hashLibrary === null ? void 0 : await hashLibrary.readSkillProbe(opName);
1053
- const landed = landedProbe !== void 0 && isPresent(landedProbe) ? landedProbe.value : null;
1054
- if (landed !== null) preRunHashes.set(opName, contentHash(landed));
1055
- }
1056
- } else {
1057
- ok = false;
1058
- failedOps.push(`skill ${op.action ?? "patch"} ${op.name}: ${result?.message ?? "service unavailable"}`);
1059
- }
1060
- }
1061
- if (failedOps.length > 0) ctx.logger.warn(`dsh-evolution-review: executePlan ${actions.length === 0 ? "no op landed" : `${actions.length} op(s) landed, ${failedOps.length} failed`}: ${failedOps.join("; ")}`);
1062
- return {
1063
- actions,
1064
- ok,
1065
- failedOps
1066
- };
1067
- } catch (error) {
1068
- const reason = error instanceof Error ? error.message : String(error);
1069
- ctx.logger.warn(`dsh-evolution-review: executePlan aborted mid-way after ${actions.length} ops landed: ${reason}`);
1070
- return {
1071
- actions,
1072
- ok: false,
1073
- failedOps,
1074
- aborted: reason
1075
- };
1076
- }
1077
- async function runApproved(kind, summary, stored, runnerArgs, approvalSession) {
1078
- if (!approval) return void 0;
1079
- if (approval.isEnabled === true && !approval.hasRunner(kind)) {
1080
- ctx.logger.warn(`dsh-evolution-review: approval enabled but no replay runner registered for kind "${kind}" - skipping write (${summary})`);
1081
- return {
1082
- ok: false,
1083
- message: `已启用批准,但没有为类型「${kind}」注册回放运行器;已跳过写入(请挂载提供该能力的工具,或禁用批准)。`
1084
- };
1085
- }
1086
- const decision = await approval.request({
1087
- kind,
1088
- summary,
1089
- args: stored,
1090
- origin: origins.approval,
1091
- ...sessionId ? { sessionId } : {},
1092
- ...approvalSession ? { session: approvalSession } : {}
1093
- });
1094
- if (decision.action === "staged") return {
1095
- ok: false,
1096
- message: decision.message
1097
- };
1098
- if (approval.isEnabled === false) return await runnerDirect(kind, runnerArgs);
1099
- return await approval.run(kind, runnerArgs, { interface: "background_review" });
1100
- }
1101
- /** Direct execution for the approval-disabled case (parallel to executeSkillDirect). */
1102
- async function runnerDirect(kind, args) {
1103
- if (kind === "memory") {
1104
- const memory = ctx.get("memory");
1105
- const op = args;
1106
- if (!memory?.applyBatch) return void 0;
1107
- return await memory.applyBatch(op.target === "user" ? "user" : "memory", [{
1108
- action: op.action ?? "add",
1109
- facts: payloadText(op),
1110
- old_text: op.old_text
1111
- }]);
1112
- }
1113
- const wrapped = args ?? {};
1114
- if (!wrapped.operation) return void 0;
1115
- return await executeSkillDirect(wrapped.operation);
1116
- }
1117
- /**
1118
- * Approval-disabled path: execute the skill op through SkillLibrary with an
1119
- * EXPLICIT background_review origin. Going through ctx.tools.execute would
1120
- * make tool-skill-manage infer origin from the parent agent's header
1121
- * (not 'subagent'), silently escaping the .hermes-managed marker and the
1122
- * pinned write guard.
1123
- */
1124
- async function executeSkillDirect(skillArgs) {
1125
- const io = ctx.get("evolutionIo");
1126
- if (!io) return {
1127
- ok: false,
1128
- message: "evolution-io 服务未挂载"
1129
- };
1130
- const policySnapshot = policySnapshotOf(ctx.get("evolutionPolicy"));
1131
- const library = newSkillLibrary({
1132
- authority: authorityFrom({ origin: resolveOrigins(void 0, true).library }),
1133
- config: rootConfig,
1134
- io: evolutionIoAdapter(() => io.provider()),
1135
- limits: {
1136
- ...DEFAULT_SKILL_LIMITS,
1137
- ...policyStageLimits(policySnapshot),
1138
- maxSkillContentChars: policySnapshot?.skillContentChars ?? DEFAULT_SKILL_LIMITS.maxSkillContentChars
1139
- },
1140
- ctx
1141
- });
1142
- const op = skillArgs;
1143
- const name = op.name ?? "";
1144
- const origin = origins.library;
1145
- const protectedNames = policySnapshot?.protectedSkillNames;
1146
- if (origin !== "foreground" && protectedNames?.includes(name) && op.action !== "create") return {
1147
- ok: false,
1148
- message: `技能「${name}」受当前策略保护(protectedSkillNames):拒绝自主写入。`
1149
- };
1150
- if (op.action === "create") {
1151
- const created = await library.create(name, op.content ?? "", origin);
1152
- if (created.ok) await ctx.get("skillUsage")?.markAgentCreated?.(name);
1153
- return created;
1154
- }
1155
- if (op.action === "edit" || op.action === "update") {
1156
- const updated = await library.update(name, op.content ?? "", origin, anchorOf(op));
1157
- if (updated.stale === true) return staleRefusal(updated, name, op.file_path);
1158
- if (updated.ok && updated.noop !== true) await ctx.get("skillUsage")?.record?.(name, "patch").catch(() => {});
1159
- return updated;
1160
- }
1161
- if (op.action === "patch") {
1162
- const patched = await library.patch(name, op.old_string ?? "", op.new_string ?? "", op.file_path ?? "", op.replace_all === true, origin);
1163
- if (patched.ok && patched.noop !== true) await ctx.get("skillUsage")?.record?.(name, "patch").catch(() => {});
1164
- return patched;
1165
- }
1166
- if (op.action === "delete") {
1167
- const into = (op.absorbed_into ?? "").trim();
1168
- if (!into || !await library.read(into)) return {
1169
- ok: false,
1170
- message: "删除需要一个已存在的 absorbed_into 目标"
1171
- };
1172
- const archived = await library.archive(name, { absorbedInto: into });
1173
- if (archived.ok) await ctx.get("skillUsage")?.markArchived?.(name);
1174
- return archived;
1175
- }
1176
- if (op.action === "write_file" || op.action === "remove_file") {
1177
- const anchor = anchorOf(op);
1178
- if (op.action === "write_file") {
1179
- const written = await library.writeSupportFile(name, op.file_path ?? "", op.file_content ?? "", origin, anchor);
1180
- if (written.stale === true) return staleRefusal(written, name, op.file_path);
1181
- return written;
1182
- }
1183
- const removedSupport = await library.removeSupportFile(name, op.file_path ?? "", origin, anchor);
1184
- if (removedSupport.stale === true) return staleRefusal(removedSupport, name, op.file_path);
1185
- if (removedSupport.ok && removedSupport.noop !== true) await ctx.get("skillUsage")?.record?.(name, "patch").catch(() => {});
1186
- return removedSupport;
1187
- }
1188
- if (op.action === "restructure") {
1189
- const moves = (op.restructure ?? []).filter((move) => move !== null).map((move) => ({
1190
- heading: move.heading ?? "",
1191
- toFile: move.to_file ?? ""
1192
- }));
1193
- return await library.restructure(name, moves, origin);
1194
- }
1195
- return {
1196
- ok: false,
1197
- message: `未知的技能动作「${op.action ?? ""}」`
1198
- };
1199
- }
740
+ return true;
1200
741
  }
1201
742
  ctx.effect(() => () => {
1202
- fiberAbort.abort();
1203
743
  sessionState.dispose();
1204
744
  }, "dsh-evolution-review.cleanup");
1205
745
  }
@@ -1222,11 +762,6 @@ function shouldCompletionReview(reason, sessionToolCalls, minToolCalls) {
1222
762
  * @param op - one accepted plan op.
1223
763
  * @returns the anchor, or `undefined` when the op carries none.
1224
764
  */
1225
- function anchorOf(op) {
1226
- const expected = op.staged_from_sha256;
1227
- if (typeof expected !== "string" || expected === "") return void 0;
1228
- return expected === "absent" ? { absent: true } : { sha256: expected };
1229
- }
1230
765
  /**
1231
766
  * Translate the library's anchor refusal into this channel's staging wording.
1232
767
  * The library reports WHICH drift it saw (drift / missing / unverifiable); the
@@ -1239,16 +774,6 @@ function anchorOf(op) {
1239
774
  * @param filePath - the support-file path for file ops; omitted for body ops.
1240
775
  * @returns the refusal to return from the direct path.
1241
776
  */
1242
- function staleRefusal(result, name, filePath) {
1243
- if (filePath === void 0 && result.anchor === "missing") return {
1244
- ok: false,
1245
- message: `技能「${name}」不存在。`
1246
- };
1247
- return {
1248
- ok: false,
1249
- message: filePath === void 0 ? `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.` : `Support file "${filePath}" 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.`
1250
- };
1251
- }
1252
777
  /** Map/set size that triggers a dead-session counter sweep (bounded, not a hard cap). */
1253
778
  const COUNTER_SWEEP_THRESHOLD = 128;
1254
779
  /**
@@ -1295,13 +820,6 @@ function sweepDeadSessionEntries(entries, isAlive) {
1295
820
  }
1296
821
  return removed;
1297
822
  }
1298
- function fingerprintPolicy(snapshot) {
1299
- try {
1300
- return createHash("sha256").update(JSON.stringify(snapshot)).digest("hex").slice(0, 12);
1301
- } catch {
1302
- return;
1303
- }
1304
- }
1305
823
  /**
1306
824
  * The call one log event ANSWERS, in either platform vocabulary.
1307
825
  *
@@ -1321,128 +839,5 @@ function resultCallIdOf(event) {
1321
839
  if (typeof data.subCallId === "string" && typeof data.isError === "boolean") return data.subCallId;
1322
840
  return null;
1323
841
  }
1324
- /**
1325
- * V10-10 (P2-11) / A1 (audit P1-1): render one `[result]` evidence line from
1326
- * a tool-result event payload. The former read (`data.output`) targeted a
1327
- * field that does not exist on the rc.2 payload; the current contract covers
1328
- * BOTH rc.2 result shapes:
1329
- * - native `tool/result`: the outcome lives in `message.content`
1330
- * tool-result blocks (inner text blocks joined); a failure is marked by
1331
- * the payload-level `error` OR a block-level `isError`;
1332
- * - PTC `tool/ptc-dispatch` settle: the outcome lives at the TOP level —
1333
- * `content` is the logged ContentBlock list and `isError` the flag; there
1334
- * is no `message` wrapper. (Before A1 this shape rendered an empty line,
1335
- * so every PTC session's evidence block starved while its plan prompt
1336
- * still demanded evidence.)
1337
- * The legacy pre-rc.2 shape (neither `message` nor a PTC settle marker) is
1338
- * tolerated as an empty payload — it never throws. `identity`, when given, is
1339
- * the dispatched tool's name + raw arguments, prepended so a settled dispatch
1340
- * keeps the call identity its `[call]` line would have had. Budget: 500 chars
1341
- * per line (the 12-line cap lives in buildReviewRequest and is unchanged).
1342
- */
1343
- function renderToolResultLine(data, identity, seq) {
1344
- const shape = data;
1345
- const nativeContent = shape?.message?.content;
1346
- const resultBlocks = Array.isArray(nativeContent) ? nativeContent.filter((block) => block.type === "tool-result") : [];
1347
- let output;
1348
- let failed;
1349
- if (shape?.message !== void 0) {
1350
- output = resultBlocks.map((block) => Array.isArray(block.content) ? block.content.map((inner) => inner.type === "text" && typeof inner.text === "string" ? inner.text : "").join(" ") : typeof block.text === "string" ? block.text : "").join(" ").trim();
1351
- failed = Boolean(shape.error) || resultBlocks.some((block) => block.isError === true);
1352
- } else if (typeof shape?.subCallId === "string" && typeof shape.isError === "boolean") {
1353
- output = textOfLoggedContent(shape.content);
1354
- failed = shape.isError;
1355
- } else {
1356
- output = "";
1357
- failed = false;
1358
- }
1359
- const head = identity === void 0 ? "" : `${foldUntrustedLines(identity.name, 0)} ${foldUntrustedLines(identity.argsRaw, 200)} → `;
1360
- const failure = failed ? " [ERROR]" : "";
1361
- return (seq === void 0 ? "" : "seq=" + String(seq) + " ") + "[result]" + failure + " " + head + foldUntrustedLines(output, 500);
1362
- }
1363
- /** Text of a PTC settle `content` payload (the logged ContentBlock list):
1364
- * text blocks and plain strings joined, anything else skipped. */
1365
- function textOfLoggedContent(content) {
1366
- if (typeof content === "string") return content.trim();
1367
- if (!Array.isArray(content)) return "";
1368
- return content.map((block) => {
1369
- if (typeof block === "string") return block;
1370
- if (block !== null && typeof block === "object") {
1371
- const candidate = block;
1372
- if (candidate.type === "text" && typeof candidate.text === "string") return candidate.text;
1373
- }
1374
- return "";
1375
- }).join(" ").trim();
1376
- }
1377
- /**
1378
- * PLAN S4.1 (2026-09-16, audit P2-12): text of one persisted content block,
1379
- * or `''` for any other shape. Content blocks cross the durable session-log
1380
- * boundary, so their runtime shape is `unknown` even where the static type
1381
- * promises `{ type, text }` — a persisted `content: [null]` (the A2-7 shape)
1382
- * used to TypeError in buildReviewRequest and the caller's catch dropped the
1383
- * whole subagent review leg. This mirrors evolution-core signals.ts's private
1384
- * `textOfBlock` (same guard, same rationale); it is not imported because core
1385
- * keeps that helper module-private, and this file's other block renderer
1386
- * (renderToolResultLine) guards its own inner-block shapes inline.
1387
- */
1388
- function textOfPersistedBlock(block) {
1389
- if (block === null || typeof block !== "object") return "";
1390
- const candidate = block;
1391
- return candidate.type === "text" && typeof candidate.text === "string" ? candidate.text : "";
1392
- }
1393
- function buildReviewRequest(session, kind, signal, maxMessages, maxMessageChars, dispatchTail = []) {
1394
- const messages = [];
1395
- const eligible = session.deriveMessages().filter((message) => message.role === "user" || message.role === "assistant").slice(-maxMessages);
1396
- for (const message of eligible) {
1397
- const text = message.content.map(textOfPersistedBlock).join(" ").trim();
1398
- if (text) messages.push(`${message.role.toUpperCase()}: ${foldUntrustedLines(text, maxMessageChars)}`);
1399
- }
1400
- const toolLines = [];
1401
- const events = dispatchTail;
1402
- const callIdentity = /* @__PURE__ */ new Map();
1403
- for (let index = events.length - 1; index >= 0; index -= 1) {
1404
- const event = events[index];
1405
- const opened = readDispatchSignal(event);
1406
- if (opened === null || callIdentity.has(opened.callId)) continue;
1407
- callIdentity.set(opened.callId, {
1408
- name: opened.name,
1409
- argsRaw: typeof opened.arguments === "string" ? opened.arguments : JSON.stringify(opened.arguments ?? {})
1410
- });
1411
- }
1412
- const openedCallIds = /* @__PURE__ */ new Set();
1413
- for (let index = events.length - 1; index >= 0 && toolLines.length < 12; index -= 1) {
1414
- const event = events[index];
1415
- const answeredCallId = resultCallIdOf(event);
1416
- if (answeredCallId !== null) {
1417
- if (openedCallIds.has(answeredCallId)) continue;
1418
- openedCallIds.add(answeredCallId);
1419
- const rawSettle = event?.data;
1420
- const settleData = rawSettle !== null && typeof rawSettle === "object" ? rawSettle : void 0;
1421
- const identity = settleData !== void 0 && typeof settleData.name === "string" && settleData.name !== "" ? {
1422
- name: settleData.name,
1423
- argsRaw: typeof settleData.arguments === "string" ? settleData.arguments : JSON.stringify(settleData.arguments ?? {})
1424
- } : callIdentity.get(answeredCallId);
1425
- toolLines.push(renderToolResultLine(event?.data, identity, event?.seq));
1426
- continue;
1427
- }
1428
- const opened = readDispatchSignal(event);
1429
- if (opened === null || openedCallIds.has(opened.callId)) continue;
1430
- openedCallIds.add(opened.callId);
1431
- const argsRaw = typeof opened.arguments === "string" ? opened.arguments : JSON.stringify(opened.arguments ?? {});
1432
- const seq = typeof event?.seq === "number" ? "seq=" + String(event.seq) + " " : "";
1433
- toolLines.push(seq + "[call] " + foldUntrustedLines(opened.name, 0) + " " + foldUntrustedLines(argsRaw, 500));
1434
- }
1435
- toolLines.reverse();
1436
- return [
1437
- `Review kind: ${kind}`,
1438
- `Signals: ${signal.toolCalls} tool calls, ${signal.userChars} user chars, ${signal.assistantChars} assistant chars.`,
1439
- `Recent tool activity (${toolLines.length}):`,
1440
- ...toolLines,
1441
- "Return ONLY the structured JSON plan. Evidence is mandatory for every op.",
1442
- planContractPrompt(),
1443
- "",
1444
- ...messages
1445
- ].join("\n");
1446
- }
1447
842
  //#endregion
1448
- export { Config, REVIEW_OUTPUT_SCHEMA, apply, buildReviewRequest, clampReviewConfig, filterUnreadSkillOps, inject, name, noticeAllowed, renderToolResultLine, resolveReviewRowSettings, shouldCompletionReview, sweepDeadSessionEntries };
843
+ export { Config, apply, clampReviewConfig, filterUnreadSkillOps, inject, name, noticeAllowed, resolveReviewRowSettings, shouldCompletionReview, sweepDeadSessionEntries };
@@ -12,8 +12,6 @@ declare module '@deepseek-ai/dsh-llm' {
12
12
  } & ContextFormed;
13
13
  }
14
14
  }
15
- import type { Session } from '@deepseek-ai/dsh-session';
16
- import { type ReviewKind } from '@lmzhen/dsh-evolution-core';
17
15
  export { filterUnreadSkillOps } from '@lmzhen/dsh-evolution-core';
18
16
  /** S2-6 (FLOW1-1): the settle-grace margin a subagent handle gets beyond its
19
17
  * own deadline before the review abandons it. As the dispose watchdog's WHOLE
@@ -41,7 +39,7 @@ export interface Config {
41
39
  * at full price. Deployments that prefer the clean-context split switch the
42
40
  * POLICY row (`evolution-policy`): the mounted policy snapshot shadows this
43
41
  * row's value, which the plugin reports once at load (v37 P2-24). */
44
- reviewMode?: Volatile<'subagent' | 'inject'>;
42
+ reviewMode?: Volatile<'inject' | 'branch'>;
45
43
  /** Review cadence in TOOL CALLS — the CANONICAL registry id (G1, signals.ts:
46
44
  * a turn advances by its tool-call count, minimum 1, or by 1 when the turn
47
45
  * itself carried the memory signal). Shadowed by the policy snapshot in every
@@ -57,22 +55,7 @@ export interface Config {
57
55
  /** Deprecated alias of `reviewSkillInterval` (G0/S0.3): still readable,
58
56
  * refused by writes; removal was planned for 0.7.0 and is deferred (the alias still ships — see the package README). */
59
57
  skillInterval?: number;
60
- /**
61
- * Tools the one-shot review subagent may use. Only actually-existing tools
62
- * may be listed (the DSH tool catalog has `skill`; the `skill_search` /
63
- * `skill_load` discovery pair does not exist on this platform).
64
- */
65
- reviewToolAllow?: string[];
66
58
  reviewTimeoutMs?: number;
67
- reviewContextMessages?: number;
68
- reviewMessageChars?: number;
69
- /** ABSOLUTE cap of the review subagent's own delegation depth (platform
70
- * resolveChildDepth: childDepth = parentDepth+1 must be <= maxDepth).
71
- * 1 permits the subagent itself and denies nesting (2 > 1); 0 rejects the
72
- * spawn outright (SubagentDepthError on any real run — the 0.3.1 defect). */
73
- reviewMaxDepth?: number;
74
- /** LLM provider for review subagents. Omit to inherit the deployment default route. */
75
- reviewProvider?: string;
76
59
  /** E3/P1-8 (v11) — TRUE semantics: this flag gates ONLY the completion
77
60
  * channel. The cadence latch stays active regardless of the value (a
78
61
  * completion-gated deployment still fires at the policy cadence), so
@@ -112,17 +95,12 @@ export interface Config {
112
95
  }
113
96
  export declare const Config: z<Schemastery.ObjectS<NoInfer<{
114
97
  reviewEnabled: z<boolean, boolean, "volatile-defined">;
115
- reviewMode: z<"subagent" | "inject", "subagent" | "inject", "volatile-defined">;
98
+ reviewMode: z<"inject" | "branch", "inject" | "branch", "volatile-defined">;
116
99
  reviewMemoryInterval: z<number, number, "volatile">;
117
100
  reviewSkillInterval: z<number, number, "volatile">;
118
101
  memoryInterval: z<number, number, "defined">;
119
102
  skillInterval: z<number, number, "defined">;
120
- reviewToolAllow: z<string[], string[], "defined">;
121
103
  reviewTimeoutMs: z<number, number, "defined">;
122
- reviewContextMessages: z<number, number, "defined">;
123
- reviewMessageChars: z<number, number, "defined">;
124
- reviewMaxDepth: z<number, number, "defined">;
125
- reviewProvider: z<string, string, "plain">;
126
104
  skillReviewTrigger: z<"cadence" | "completion" | "both", "cadence" | "completion" | "both", "volatile-defined">;
127
105
  reviewWakeInject: z<boolean, boolean, "volatile-defined">;
128
106
  reviewNotice: z<boolean, boolean, "volatile-defined">;
@@ -132,17 +110,12 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
132
110
  skillsRoot: z<string, string, "defined">;
133
111
  }>>, Schemastery.ObjectT<NoInfer<{
134
112
  reviewEnabled: z<boolean, boolean, "volatile-defined">;
135
- reviewMode: z<"subagent" | "inject", "subagent" | "inject", "volatile-defined">;
113
+ reviewMode: z<"inject" | "branch", "inject" | "branch", "volatile-defined">;
136
114
  reviewMemoryInterval: z<number, number, "volatile">;
137
115
  reviewSkillInterval: z<number, number, "volatile">;
138
116
  memoryInterval: z<number, number, "defined">;
139
117
  skillInterval: z<number, number, "defined">;
140
- reviewToolAllow: z<string[], string[], "defined">;
141
118
  reviewTimeoutMs: z<number, number, "defined">;
142
- reviewContextMessages: z<number, number, "defined">;
143
- reviewMessageChars: z<number, number, "defined">;
144
- reviewMaxDepth: z<number, number, "defined">;
145
- reviewProvider: z<string, string, "plain">;
146
119
  skillReviewTrigger: z<"cadence" | "completion" | "both", "cadence" | "completion" | "both", "volatile-defined">;
147
120
  reviewWakeInject: z<boolean, boolean, "volatile-defined">;
148
121
  reviewNotice: z<boolean, boolean, "volatile-defined">;
@@ -151,30 +124,6 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
151
124
  root: z<string, string, "defined">;
152
125
  skillsRoot: z<string, string, "defined">;
153
126
  }>>, "plain">;
154
- /** V8-01 (0.3.45): the review subagent's structured-output contract. The
155
- * `subagents.start` outputSchema is RAW JSON Schema validated by
156
- * assertObjectJsonSchema (upstream dsh-tools json-schema.ts — SCHEMA_TYPES
157
- * has no 'json'; 'json' is a defineTool-author DSL-only value). The arrays
158
- * deliberately use `{ type: 'array' }` with NO items node — absent items
159
- * accepts any JSON item (upstream semantics, json-schema.ts "Item schema
160
- * (type: 'array' only); absent accepts any JSON item"), and plan-validator
161
- * owns the per-op structure proof. The whitelist contract test (review.spec
162
- * V8-01) forbids a DSL value slipping back in. */
163
- export declare const REVIEW_OUTPUT_SCHEMA: {
164
- readonly type: "object";
165
- readonly additionalProperties: false;
166
- readonly properties: {
167
- readonly memoryOps: {
168
- readonly type: "array";
169
- };
170
- readonly skillOps: {
171
- readonly type: "array";
172
- };
173
- readonly summary: {
174
- readonly type: "string";
175
- };
176
- };
177
- };
178
127
  /** F5 (P2-16, v11): the clamp sets exactly these four numeric fields — the
179
128
  * old `as Required<Config>` lied about `reviewToolAllow` etc. being populated
180
129
  * (`[...undefined]` TypeErrors under a direct `apply(ctx, {})`).
@@ -182,9 +131,6 @@ export declare const REVIEW_OUTPUT_SCHEMA: {
182
131
  * {@link resolveReviewRowSettings}, which clamps its own numerics. */
183
132
  type ClampedReviewConfig = Config & {
184
133
  reviewTimeoutMs: number;
185
- reviewContextMessages: number;
186
- reviewMessageChars: number;
187
- reviewMaxDepth: number;
188
134
  };
189
135
  /**
190
136
  * The DEPLOYMENT layer of this row, clamped once at assembly: the restart-only
@@ -213,7 +159,7 @@ export interface ReviewSettings {
213
159
  /** Master switch for the review plugin. */
214
160
  reviewEnabled: boolean;
215
161
  /** Run the review in the parent session (inject) or on a subagent. */
216
- reviewMode: 'subagent' | 'inject';
162
+ reviewMode: 'inject' | 'branch';
217
163
  /** Deliver the deferred review as a waking follow-up message. */
218
164
  reviewWakeInject: boolean;
219
165
  /** Deliver result notices (not review prompts). */
@@ -235,13 +181,6 @@ export declare function apply(ctx: Context, rawConfig?: Config): void;
235
181
  export declare function shouldCompletionReview(reason: {
236
182
  kind?: string;
237
183
  } | undefined, sessionToolCalls: number, minToolCalls: number): boolean;
238
- /** One dispatch frame the review digest can render (a native call/result or a PTC sub-dispatch). */
239
- interface ReviewDispatchFrame {
240
- readonly type: string;
241
- /** Session seq of the frame, so a plan can cite a real `event_seq`. */
242
- readonly seq?: number;
243
- readonly data?: unknown;
244
- }
245
184
  /**
246
185
  * Remove every entry whose session is no longer live (rc.42 audit P1-10):
247
186
  * `cumulativeToolCalls` / `completionInjected` / `pendingCadenceReviews` are keyed by
@@ -250,32 +189,4 @@ interface ReviewDispatchFrame {
250
189
  * number of removed entries.
251
190
  */
252
191
  export declare function sweepDeadSessionEntries<K>(entries: Map<K, unknown> | Set<K>, isAlive: (id: K) => boolean): number;
253
- /**
254
- * V10-10 (P2-11) / A1 (audit P1-1): render one `[result]` evidence line from
255
- * a tool-result event payload. The former read (`data.output`) targeted a
256
- * field that does not exist on the rc.2 payload; the current contract covers
257
- * BOTH rc.2 result shapes:
258
- * - native `tool/result`: the outcome lives in `message.content`
259
- * tool-result blocks (inner text blocks joined); a failure is marked by
260
- * the payload-level `error` OR a block-level `isError`;
261
- * - PTC `tool/ptc-dispatch` settle: the outcome lives at the TOP level —
262
- * `content` is the logged ContentBlock list and `isError` the flag; there
263
- * is no `message` wrapper. (Before A1 this shape rendered an empty line,
264
- * so every PTC session's evidence block starved while its plan prompt
265
- * still demanded evidence.)
266
- * The legacy pre-rc.2 shape (neither `message` nor a PTC settle marker) is
267
- * tolerated as an empty payload — it never throws. `identity`, when given, is
268
- * the dispatched tool's name + raw arguments, prepended so a settled dispatch
269
- * keeps the call identity its `[call]` line would have had. Budget: 500 chars
270
- * per line (the 12-line cap lives in buildReviewRequest and is unchanged).
271
- */
272
- export declare function renderToolResultLine(data: unknown, identity?: {
273
- name: string;
274
- argsRaw: string;
275
- }, seq?: number): string;
276
- export declare function buildReviewRequest(session: Session, kind: ReviewKind, signal: {
277
- toolCalls: number;
278
- userChars: number;
279
- assistantChars: number;
280
- }, maxMessages: number, maxMessageChars: number, dispatchTail?: readonly ReviewDispatchFrame[]): string;
281
192
  //# sourceMappingURL=index.d.ts.map
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.30.28",
4
+ "version": "0.30.30",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -27,9 +27,7 @@
27
27
  "license": "MIT",
28
28
  "dependencies": {
29
29
  "@deepseek-ai/schemastery": "^3.18.5",
30
- "@lmzhen/dsh-evolution-approval": "^0.30.28",
31
- "@lmzhen/dsh-evolution-core": "^0.30.28",
32
- "@lmzhen/dsh-evolution-plan-validator": "^0.30.28"
30
+ "@lmzhen/dsh-evolution-core": "^0.30.30"
33
31
  },
34
32
  "peerDependencies": {
35
33
  "@deepseek-ai/cordis": "^4.0.5",
@@ -37,22 +35,17 @@
37
35
  "@deepseek-ai/dsh-llm": "^0.2.1-rc.2",
38
36
  "@deepseek-ai/dsh-session": "^0.2.1-rc.2",
39
37
  "@deepseek-ai/dsh-tools": "^0.2.1-rc.2",
40
- "@lmzhen/dsh-evolution-policy": "^0.30.28",
41
- "@lmzhen/dsh-evolution-state": "^0.30.28"
38
+ "@lmzhen/dsh-evolution-policy": "^0.30.30",
39
+ "@lmzhen/dsh-evolution-state": "^0.30.30"
42
40
  },
43
41
  "devDependencies": {
44
42
  "@deepseek-ai/dsh-agent": "^0.2.1-rc.2",
45
43
  "@deepseek-ai/dsh-agent-loop-testkit": "^0.2.1-rc.2",
46
44
  "@deepseek-ai/dsh-llm": "^0.2.1-rc.2",
47
45
  "@deepseek-ai/dsh-session": "^0.2.1-rc.2",
48
- "@deepseek-ai/dsh-session-persistence": "^0.2.1-rc.2",
49
- "@deepseek-ai/dsh-session-persistence-jsonl": "^0.2.1-rc.2",
50
46
  "@deepseek-ai/dsh-tools": "^0.2.1-rc.2",
51
- "@lmzhen/dsh-evolution-approval": "^0.30.28",
52
- "@lmzhen/dsh-evolution-core": "^0.30.28",
53
- "@lmzhen/dsh-evolution-curator": "^0.30.28",
54
- "@lmzhen/dsh-evolution-plan-validator": "^0.30.28",
55
- "@lmzhen/dsh-evolution-state": "^0.30.28"
47
+ "@lmzhen/dsh-evolution-core": "^0.30.30",
48
+ "@lmzhen/dsh-evolution-state": "^0.30.30"
56
49
  },
57
50
  "engines": {
58
51
  "dsh": ">=0.2.1-rc.2 <0.2.2-0"