@lmzhen/dsh-evolution-review 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -17,7 +17,7 @@ own, and the delivery mode lives on the `evolution-policy` row, not here.
17
17
  - 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).
18
18
  - 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.
19
19
  - 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.
20
- - Read-before-write tracks only reads through the `skill` tool. `skill_manage` has no per-skill read action (`list`/`review` are whole-library, not targeted at one name), so a skill that was only listed via `skill_manage` is not marked as read: a background review may still reject a patch to it until it is actually loaded.
20
+ - Read-before-write (the rule is `evolution-core`'s `skill-reads.ts`, shared with the tool path's admission gate) tracks only reads through the `skill` tool. `skill_manage` has no per-skill read action (`list`/`review` are whole-library, not targeted at one name), so a skill that was only listed via `skill_manage` is not marked as read: a background review may still reject a patch to it until it is actually loaded.
21
21
  - The completion-channel counters (`cumulativeToolCalls` / `completionInjected`) are in-memory only. A process restart resets them, which is accepted behavior: the completion review is a one-per-session post-task adaptation and a restart is treated as a fresh conversation boundary. The cadence state (`turnsSinceMemory` / `turnsSinceSkill`) is persisted via `ReviewState` and survives restart: bounded by `REVIEW_STATE_SESSION_CAP` (500, seam constant): the least-recently-active sessions are pruned on save, so a very old session restarting resumes from a fresh cadence baseline rather than an unbounded store.
22
22
  - `evolution/review-scheduled` and `evolution/review-error` are emitted for platform/user wiring only: this family has no in-repo production `ctx.on` consumer for them. They are declared externally owned (the platform side wires consumption), which matches the `EXEMPT_ORPHANS` set in `scripts/verify-event-pairing.mjs`.
23
23
  - When the `evolution-state` service is not mounted, the memory/skill cadence state is not persisted and every turn restarts from a clean `{ turnsSinceMemory: 0, turnsSinceSkill: 0 }` baseline: the review schedule is stateless and re-decided each turn rather than accumulating across the conversation. The loss is surfaced once per process as a logger warning at the first turn/end.
package/lib/index.js CHANGED
@@ -2,7 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
4
4
  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, PARAM_NAMESPACES, PROMPT_BUNDLE, advanceReview, assertSkillsRootAliasRetired, clampedNumber, clearReviewChannel, contentHash, evolutionIoAdapter, foldToolDispatches, foldTurn, installParamSection, markReviewChannel, newSkillLibrary, policyStageLimits, readDispatchSignal, readNumberParam, redactSecrets, resolveOrigins, resolveRootConfig, reviewPrompt, sessionAudited, skillReadNameOf, sweepReviewChannelSessions, verifyPromptBundle } from "@lmzhen/dsh-evolution-core";
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, clampedNumber, clearReviewChannel, collectReadSkillNames, contentHash, evolutionIoAdapter, filterUnreadSkillOps, filterUnreadSkillOps as filterUnreadSkillOps$1, foldTurn, installParamSection, markReviewChannel, newSkillLibrary, paramNamespace, policyStageLimits, readDispatchSignal, readNumberParam, redactSecrets, resolveOrigins, resolveRootConfig, reviewPrompt, sessionAudited, sweepReviewChannelSessions, verifyPromptBundle } from "@lmzhen/dsh-evolution-core";
6
6
  import { validateEvolutionPlan } from "@lmzhen/dsh-evolution-plan-validator";
7
7
  //#region lib/types/session-state.js
8
8
  /**
@@ -152,8 +152,6 @@ function clampReviewConfig(rawConfig, ctx) {
152
152
  function policySnapshotOf(source) {
153
153
  return source?.get?.();
154
154
  }
155
- /** Namespace the review group's user-writable knobs live in (core's PARAM_NAMESPACES). */
156
- const REVIEW_SETTINGS_NAMESPACE = "evolution-review";
157
155
  /** Schema the platform validates the user layer against; defaults mirror the
158
156
  * core constants so an empty document resolves to today's behaviour. */
159
157
  const REVIEW_SETTINGS_SCHEMA = z.object({
@@ -196,7 +194,7 @@ function apply(ctx, rawConfig = {}) {
196
194
  reviewMode: config.reviewMode ?? "inject",
197
195
  reviewWakeInject: config.reviewWakeInject ?? true
198
196
  };
199
- const overrides = installParamSection(ctx, PARAM_NAMESPACES["evolution-review"] ?? "evolution-review", REVIEW_SETTINGS_SCHEMA, settingsBase, { warn: (message) => {
197
+ const overrides = installParamSection(ctx, paramNamespace("evolution-review"), REVIEW_SETTINGS_SCHEMA, settingsBase, { warn: (message) => {
200
198
  ctx.logger.warn("dsh-evolution-review: " + message);
201
199
  } });
202
200
  const params = () => {
@@ -663,7 +661,7 @@ function apply(ctx, rawConfig = {}) {
663
661
  ctx.logger.warn(`dsh-evolution-review: review subagent returned no structured plan${stopDetail !== void 0 ? ` (stopReason=${stopDetail}${diagDetail !== void 0 ? `; diagnostic=${diagDetail}` : ""})` : ""}`);
664
662
  return false;
665
663
  }
666
- const childReads = run.localAgent ? collectReadSkillNames(run.localAgent.session) : /* @__PURE__ */ new Set();
664
+ const childReads = run.localAgent ? collectReadSkillNames(run.localAgent.session.snapshotEvents()) : /* @__PURE__ */ new Set();
667
665
  const plan = result.structured;
668
666
  const policyFingerprint = fingerprintPolicy(snapshot);
669
667
  const validation = validateEvolutionPlan(plan, {
@@ -675,7 +673,7 @@ function apply(ctx, rawConfig = {}) {
675
673
  maxSkillContentChars: snapshot?.skillContentChars ?? DEFAULT_SKILL_CONTENT_CHARS
676
674
  });
677
675
  const acceptedSkillOps = validation.accepted.skillOps ?? [];
678
- const skippedUnread = filterUnreadSkillOps(acceptedSkillOps, new Set([...collectReadSkillNames(session), ...childReads]));
676
+ const skippedUnread = filterUnreadSkillOps$1(acceptedSkillOps, new Set([...collectReadSkillNames(session.snapshotEvents()), ...childReads]));
679
677
  const evidenceQuotes = [...validation.accepted.memoryOps ?? [], ...acceptedSkillOps].reduce((total, op) => total + (Array.isArray(op.evidence) ? op.evidence.length : 0), 0);
680
678
  const emitApplied = (report) => {
681
679
  try {
@@ -1056,29 +1054,6 @@ function staleRefusal(result, name, filePath) {
1056
1054
  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.`
1057
1055
  };
1058
1056
  }
1059
- /**
1060
- * v37 P7a: the read-before-write credit now comes from evolution-core's
1061
- * `tool-dispatch` module — the ONE reader of the platform's dispatch event
1062
- * types, and the ONE authority on which tool reads a skill.
1063
- *
1064
- * v32 REV-06(a) is preserved by the normalizer: a skill counts as READ only
1065
- * when it did not fail, so a failed/timeout read still cannot pass the
1066
- * read-before-write gate and let the review blind-overwrite content the model
1067
- * never saw. What changed is the vocabulary the gate listens to: matching
1068
- * `tool/call` here meant every PTC session (`tool/ptc-dispatch*`) collected an
1069
- * EMPTY set, so `filterUnreadSkillOps` dropped every mutating op the model had
1070
- * legitimately read first — and nothing reported the loss.
1071
- * @param session - the session whose log is folded.
1072
- * @returns the skill names this session read through a non-failed dispatch.
1073
- */
1074
- function collectReadSkillNames(session) {
1075
- const names = /* @__PURE__ */ new Set();
1076
- for (const dispatch of foldToolDispatches(session.snapshotEvents())) {
1077
- const name = skillReadNameOf(dispatch);
1078
- if (name !== void 0) names.add(name);
1079
- }
1080
- return names;
1081
- }
1082
1057
  /** Map/set size that triggers a dead-session counter sweep (bounded, not a hard cap). */
1083
1058
  const COUNTER_SWEEP_THRESHOLD = 128;
1084
1059
  /**
@@ -1096,35 +1071,6 @@ function sweepDeadSessionEntries(entries, isAlive) {
1096
1071
  }
1097
1072
  return removed;
1098
1073
  }
1099
- /**
1100
- * Drop mutating ops whose target was not read this session, in place.
1101
- * Create is exempt (no read required to author a new skill). Covers the same
1102
- * mutating surface Hermes guards (edit/patch/write_file/remove_file), so a
1103
- * background review cannot blind-touch support files or edits of skills it
1104
- * never loaded. Returns the count of dropped ops so the plan event can report
1105
- * them as rejected.
1106
- */
1107
- function filterUnreadSkillOps(ops, readNames) {
1108
- const READ_REQUIRED = [
1109
- "edit",
1110
- "update",
1111
- "patch",
1112
- "delete",
1113
- "write_file",
1114
- "remove_file",
1115
- "restructure"
1116
- ];
1117
- let dropped = 0;
1118
- for (let index = ops.length - 1; index >= 0; index -= 1) {
1119
- const op = ops[index];
1120
- if (!op) continue;
1121
- if (op.name && READ_REQUIRED.includes(op.action ?? "patch") && !readNames.has(op.name)) {
1122
- ops.splice(index, 1);
1123
- dropped += 1;
1124
- }
1125
- }
1126
- return dropped;
1127
- }
1128
1074
  function fingerprintPolicy(snapshot) {
1129
1075
  try {
1130
1076
  return createHash("sha256").update(JSON.stringify(snapshot)).digest("hex").slice(0, 12);
@@ -1273,4 +1219,4 @@ function buildReviewRequest(session, kind, signal, maxMessages, maxMessageChars)
1273
1219
  ].join("\n");
1274
1220
  }
1275
1221
  //#endregion
1276
- export { Config, REVIEW_OUTPUT_SCHEMA, REVIEW_SETTINGS_NAMESPACE, REVIEW_SETTINGS_SCHEMA, apply, buildReviewRequest, clampReviewConfig, filterUnreadSkillOps, inject, name, renderToolResultLine, shouldCompletionReview, sweepDeadSessionEntries };
1222
+ export { Config, REVIEW_OUTPUT_SCHEMA, REVIEW_SETTINGS_SCHEMA, apply, buildReviewRequest, clampReviewConfig, filterUnreadSkillOps, inject, name, renderToolResultLine, shouldCompletionReview, sweepDeadSessionEntries };
@@ -6,6 +6,7 @@ import type { Context } from '@deepseek-ai/cordis';
6
6
  import z from '@deepseek-ai/schemastery';
7
7
  import type { Session } from '@deepseek-ai/dsh-session';
8
8
  import { type ReviewKind } from '@lmzhen/dsh-evolution-core';
9
+ export { filterUnreadSkillOps } from '@lmzhen/dsh-evolution-core';
9
10
  export declare const name = "evolution-review";
10
11
  export declare const inject: string[];
11
12
  export interface Config {
@@ -26,6 +27,8 @@ export interface Config {
26
27
  /** Shadowed by the policy snapshot in every shipped composition — configure
27
28
  * `reviewMemoryInterval` on the `evolution-policy` row instead (v37 P2-24).
28
29
  * Deprecated alias (G0/S0.3): still readable, refused by writes; removed 0.7.0. */
30
+ /** Review cadence in TOOL CALLS (signals.ts: a turn advances by its tool-call count, minimum 1,
31
+ * or by 1 when the turn itself carried the memory signal). */
29
32
  memoryInterval?: number;
30
33
  /** Shadowed by the policy snapshot in every shipped composition — configure
31
34
  * `reviewSkillInterval` on the `evolution-policy` row instead. Deprecated
@@ -129,15 +132,15 @@ type ClampedReviewConfig = Config & {
129
132
  skillReviewCompletionMinToolCalls: number;
130
133
  };
131
134
  export declare function clampReviewConfig(rawConfig: Config, ctx: Context): ClampedReviewConfig;
132
- /** Namespace the review group's user-writable knobs live in (core's PARAM_NAMESPACES). */
133
- export declare const REVIEW_SETTINGS_NAMESPACE = "evolution-review";
134
135
  /** Review behaviour a user may change (G3/S3.1). Field names are the CANONICAL
135
136
  * parameter ids from the registry, so the settings document, the params output,
136
137
  * the doctor report and the cards all spell one name. */
137
138
  export interface ReviewSettings {
138
- /** Activity units between skill-review injections. */
139
+ /** Tool calls between skill-review injections (a turn with no tool call counts as one; a turn
140
+ * that itself used a skill advances the counter by 1). */
139
141
  reviewSkillInterval: number;
140
- /** Activity units between memory-review injections. */
142
+ /** Tool calls between memory-review injections (a turn with no tool call counts as one; a turn
143
+ * that itself touched memory advances the counter by 1). */
141
144
  reviewMemoryInterval: number;
142
145
  /** Which channel may inject a skill review. */
143
146
  skillReviewTrigger: 'cadence' | 'completion' | 'both';
@@ -166,18 +169,6 @@ export declare function shouldCompletionReview(reason: {
166
169
  * number of removed entries.
167
170
  */
168
171
  export declare function sweepDeadSessionEntries<K>(entries: Map<K, unknown> | Set<K>, isAlive: (id: K) => boolean): number;
169
- /**
170
- * Drop mutating ops whose target was not read this session, in place.
171
- * Create is exempt (no read required to author a new skill). Covers the same
172
- * mutating surface Hermes guards (edit/patch/write_file/remove_file), so a
173
- * background review cannot blind-touch support files or edits of skills it
174
- * never loaded. Returns the count of dropped ops so the plan event can report
175
- * them as rejected.
176
- */
177
- export declare function filterUnreadSkillOps(ops: Array<{
178
- action?: string;
179
- name?: string;
180
- }>, readNames: ReadonlySet<string>): number;
181
172
  /**
182
173
  * V10-10 (P2-11) / A1 (audit P1-1): render one `[result]` evidence line from
183
174
  * a tool-result event payload. The former read (`data.output`) targeted a
@@ -206,5 +197,4 @@ export declare function buildReviewRequest(session: Session, kind: ReviewKind, s
206
197
  userChars: number;
207
198
  assistantChars: number;
208
199
  }, maxMessages: number, maxMessageChars: number): string;
209
- export {};
210
200
  //# 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.7.0",
4
+ "version": "0.9.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -27,9 +27,9 @@
27
27
  "license": "MIT",
28
28
  "dependencies": {
29
29
  "@deepseek-ai/schemastery": "^3.18.1",
30
- "@lmzhen/dsh-evolution-approval": "^0.7.0",
31
- "@lmzhen/dsh-evolution-core": "^0.7.0",
32
- "@lmzhen/dsh-evolution-plan-validator": "^0.7.0"
30
+ "@lmzhen/dsh-evolution-approval": "^0.9.0",
31
+ "@lmzhen/dsh-evolution-core": "^0.9.0",
32
+ "@lmzhen/dsh-evolution-plan-validator": "^0.9.0"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "@deepseek-ai/cordis": "^4.0.1",
@@ -37,8 +37,8 @@
37
37
  "@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
38
38
  "@deepseek-ai/dsh-session": "^0.1.5-rc.2",
39
39
  "@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
40
- "@lmzhen/dsh-evolution-state": "^0.7.0",
41
- "@lmzhen/dsh-evolution-policy": "^0.7.0"
40
+ "@lmzhen/dsh-evolution-state": "^0.9.0",
41
+ "@lmzhen/dsh-evolution-policy": "^0.9.0"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@deepseek-ai/dsh-agent": "^0.1.5-rc.2",
@@ -48,10 +48,10 @@
48
48
  "@deepseek-ai/dsh-session-persistence": "^0.1.5-rc.2",
49
49
  "@deepseek-ai/dsh-session-persistence-jsonl": "^0.1.5-rc.2",
50
50
  "@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
51
- "@lmzhen/dsh-evolution-approval": "^0.7.0",
52
- "@lmzhen/dsh-evolution-core": "^0.7.0",
53
- "@lmzhen/dsh-evolution-curator": "^0.7.0",
54
- "@lmzhen/dsh-evolution-plan-validator": "^0.7.0",
55
- "@lmzhen/dsh-evolution-state": "^0.7.0"
51
+ "@lmzhen/dsh-evolution-approval": "^0.9.0",
52
+ "@lmzhen/dsh-evolution-core": "^0.9.0",
53
+ "@lmzhen/dsh-evolution-curator": "^0.9.0",
54
+ "@lmzhen/dsh-evolution-plan-validator": "^0.9.0",
55
+ "@lmzhen/dsh-evolution-state": "^0.9.0"
56
56
  }
57
57
  }