@lmzhen/dsh-evolution-review 0.3.66 → 0.3.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/lib/index.js +108 -23
  3. package/package.json +10 -10
package/README.md CHANGED
@@ -25,7 +25,7 @@ Independent of request-prefix construction. This package does not alter the asse
25
25
  - 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.
26
26
  - 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.
27
27
  - 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.
28
- - 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.
28
+ - 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.
29
29
  - `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`.
30
30
  - 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.
31
31
  - Read-before-write can see the review subagent's own `skill` reads only when the subagent backend exposes `localAgent` (the in-process driver does; out-of-process backends such as ACP and the CLI providers set `localAgent: undefined`). With a remote backend the subagent's reads are invisible, so a plan item patching a skill the subagent itself loaded is dropped as "unread" — the review then falls back to the parent session's reads only. Documented rather than worked around: recovering the child read set needs a `SubagentLike` contract change (v14 P2-6).
package/lib/index.js CHANGED
@@ -93,6 +93,16 @@ function apply(ctx, rawConfig = {}) {
93
93
  const cadenceResetWarned = /* @__PURE__ */ new Set();
94
94
  let reviewInFlight = false;
95
95
  const policy = () => ctx.get("evolutionPolicy")?.get();
96
+ const reviewStateLocks = /* @__PURE__ */ new Map();
97
+ async function withReviewStateLock(id, task) {
98
+ const next = (reviewStateLocks.get(id) ?? Promise.resolve()).catch(() => {}).then(task);
99
+ reviewStateLocks.set(id, next);
100
+ try {
101
+ return await next;
102
+ } finally {
103
+ if (reviewStateLocks.get(id) === next) reviewStateLocks.delete(id);
104
+ }
105
+ }
96
106
  ctx.on("session/event", (session, event) => {
97
107
  if (event.type === "turn/start" && session.header.origin !== "subagent") turnStarts.set(session.id, session.seq - 1);
98
108
  if (event.type !== "turn/end") return;
@@ -132,24 +142,32 @@ function apply(ctx, rawConfig = {}) {
132
142
  statelessReviewStateWarned = true;
133
143
  ctx.logger.warn("dsh-evolution-review: evolution-state service not mounted — memory/skill review cadence is not persisted and resets every turn (see README Known Limitations).");
134
144
  }
135
- const state = await stateService?.loadReviewState(session.id) ?? {
145
+ const snapshot = policy();
146
+ const skipFire = skipNextCadenceFire.get(session.id) ?? false;
147
+ if (skipFire) skipNextCadenceFire.delete(session.id);
148
+ let state = {
136
149
  turnsSinceMemory: 0,
137
150
  turnsSinceSkill: 0,
138
151
  lastTurn: -1
139
152
  };
140
- const snapshot = policy();
141
- const skipFire = skipNextCadenceFire.get(session.id) ?? false;
142
- if (skipFire) skipNextCadenceFire.delete(session.id);
143
- const rawKind = advanceReview(state, event.data.turn, signal, {
144
- memoryInterval: snapshot?.reviewMemoryInterval ?? config.memoryInterval,
145
- skillInterval: snapshot?.reviewSkillInterval ?? config.skillInterval,
146
- substantiveMinToolCalls: snapshot?.substantiveMinToolCalls ?? 3,
147
- substantiveMinUserChars: snapshot?.substantiveMinUserChars ?? 200,
148
- substantiveMinAgentChars: snapshot?.substantiveMinAgentChars ?? 500,
149
- resetOnFire: false
153
+ const advanced = { kind: null };
154
+ await withReviewStateLock(session.id, async () => {
155
+ state = await stateService?.loadReviewState(session.id) ?? {
156
+ turnsSinceMemory: 0,
157
+ turnsSinceSkill: 0,
158
+ lastTurn: -1
159
+ };
160
+ advanced.kind = advanceReview(state, event.data.turn, signal, {
161
+ memoryInterval: snapshot?.reviewMemoryInterval ?? config.memoryInterval,
162
+ skillInterval: snapshot?.reviewSkillInterval ?? config.skillInterval,
163
+ substantiveMinToolCalls: snapshot?.substantiveMinToolCalls ?? 3,
164
+ substantiveMinUserChars: snapshot?.substantiveMinUserChars ?? 200,
165
+ substantiveMinAgentChars: snapshot?.substantiveMinAgentChars ?? 500,
166
+ resetOnFire: false
167
+ });
168
+ await stateService?.saveReviewState(session.id, state);
150
169
  });
151
- const kind = skipFire ? null : rawKind;
152
- await stateService?.saveReviewState(session.id, state);
170
+ const kind = skipFire ? null : advanced.kind;
153
171
  const cumulative = (cumulativeToolCalls.get(session.id) ?? 0) + signal.toolCalls;
154
172
  cumulativeToolCalls.set(session.id, cumulative);
155
173
  if (event.data.reason.kind === "completed") {
@@ -158,6 +176,18 @@ function apply(ctx, rawConfig = {}) {
158
176
  pendingCadenceReviews.delete(session.id);
159
177
  if ((policy()?.reviewMode ?? config.reviewMode) === "inject") try {
160
178
  deliverMessage(agent, reviewPrompt(pendingKind), "auto-review");
179
+ try {
180
+ ctx.emit("evolution/review-scheduled", {
181
+ sessionId: session.id,
182
+ kind: pendingKind,
183
+ toolCalls: signal.toolCalls,
184
+ userChars: signal.userChars,
185
+ assistantChars: signal.assistantChars,
186
+ channel: "inject"
187
+ });
188
+ } catch (emitError) {
189
+ ctx.logger.warn(`dsh-evolution-review: review-scheduled emit failed: ${emitError instanceof Error ? emitError.message : String(emitError)}`);
190
+ }
161
191
  } catch (injectError) {
162
192
  ctx.logger.warn(`dsh-evolution-review: deferred review inject failed: ${injectError instanceof Error ? injectError.message : String(injectError)}`);
163
193
  }
@@ -169,21 +199,37 @@ function apply(ctx, rawConfig = {}) {
169
199
  kind: pendingKind,
170
200
  toolCalls: signal.toolCalls,
171
201
  userChars: signal.userChars,
172
- assistantChars: signal.assistantChars
202
+ assistantChars: signal.assistantChars,
203
+ channel: "subagent"
173
204
  });
174
205
  } catch (emitError) {
175
206
  ctx.logger.warn(`dsh-evolution-review: review-scheduled emit failed: ${emitError instanceof Error ? emitError.message : String(emitError)}`);
176
207
  }
177
208
  else if (reviewOutcome !== "deferred") try {
178
209
  deliverMessage(agent, reviewPrompt(pendingKind), "auto-review");
210
+ try {
211
+ ctx.emit("evolution/review-scheduled", {
212
+ sessionId: session.id,
213
+ kind: pendingKind,
214
+ toolCalls: signal.toolCalls,
215
+ userChars: signal.userChars,
216
+ assistantChars: signal.assistantChars,
217
+ channel: "inject"
218
+ });
219
+ } catch (emitError) {
220
+ ctx.logger.warn(`dsh-evolution-review: review-scheduled emit failed: ${emitError instanceof Error ? emitError.message : String(emitError)}`);
221
+ }
179
222
  } catch (injectError) {
180
223
  ctx.logger.warn(`dsh-evolution-review: deferred review inject failed: ${injectError instanceof Error ? injectError.message : String(injectError)}`);
181
224
  }
182
225
  }
183
- state.turnsSinceMemory = 0;
184
- state.turnsSinceSkill = 0;
185
226
  try {
186
- await stateService?.saveReviewState(session.id, state);
227
+ await withReviewStateLock(session.id, async () => {
228
+ const fresh = await stateService?.loadReviewState(session.id) ?? state;
229
+ fresh.turnsSinceMemory = 0;
230
+ fresh.turnsSinceSkill = 0;
231
+ await stateService?.saveReviewState(session.id, fresh);
232
+ });
187
233
  } catch (resetError) {
188
234
  if (!cadenceResetWarned.has(session.id)) {
189
235
  cadenceResetWarned.add(session.id);
@@ -207,6 +253,22 @@ function apply(ctx, rawConfig = {}) {
207
253
  if (completionInjected.has(session.id)) return;
208
254
  if (!shouldCompletionReview(event.data.reason, cumulative, config.skillReviewCompletionMinToolCalls)) return;
209
255
  completionInjected.add(session.id);
256
+ if (reviewInFlight) {
257
+ if (deferredFallbackReviews.length < DEFERRED_REVIEW_CAP) deferredFallbackReviews.push({
258
+ agent,
259
+ sessionId: session.id,
260
+ kind: "skill",
261
+ prompt: COMPLETION_SKILL_REVIEW_PROMPT,
262
+ label: "completion review",
263
+ channel: "completion",
264
+ counts: signal
265
+ });
266
+ else {
267
+ completionInjected.delete(session.id);
268
+ 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`);
269
+ }
270
+ return;
271
+ }
210
272
  try {
211
273
  deliverMessage(agent, COMPLETION_SKILL_REVIEW_PROMPT, "completion review");
212
274
  } catch (injectError) {
@@ -220,7 +282,8 @@ function apply(ctx, rawConfig = {}) {
220
282
  kind: "skill",
221
283
  toolCalls: signal.toolCalls,
222
284
  userChars: signal.userChars,
223
- assistantChars: signal.assistantChars
285
+ assistantChars: signal.assistantChars,
286
+ channel: "completion"
224
287
  });
225
288
  } catch (emitError) {
226
289
  ctx.logger.warn(`dsh-evolution-review: review-scheduled emit failed: ${emitError instanceof Error ? emitError.message : String(emitError)}`);
@@ -273,7 +336,12 @@ function apply(ctx, rawConfig = {}) {
273
336
  if (reviewInFlight) {
274
337
  if (deferredFallbackReviews.length < DEFERRED_REVIEW_CAP) deferredFallbackReviews.push({
275
338
  agent,
276
- kind
339
+ sessionId: session.id,
340
+ kind,
341
+ prompt: reviewPrompt(kind),
342
+ label: "auto-review",
343
+ channel: "inject",
344
+ counts: signal
277
345
  });
278
346
  else ctx.logger.warn(`dsh-evolution-review: deferred-review queue at cap (${DEFERRED_REVIEW_CAP}) — dropping one fallback review prompt`);
279
347
  return "deferred";
@@ -379,10 +447,26 @@ function apply(ctx, rawConfig = {}) {
379
447
  } finally {
380
448
  reviewInFlight = false;
381
449
  const deferred = deferredFallbackReviews.splice(0);
382
- for (const { agent: waitingAgent, kind: waitingKind } of deferred) try {
383
- deliverMessage(waitingAgent, reviewPrompt(waitingKind), "auto-review");
384
- } catch (injectError) {
385
- ctx.logger.warn(`dsh-evolution-review: deferred review inject failed: ${injectError instanceof Error ? injectError.message : String(injectError)}`);
450
+ for (const { agent: waitingAgent, sessionId: entrySession, kind: waitingKind, prompt, label, channel, counts: entryCounts } of deferred) {
451
+ try {
452
+ deliverMessage(waitingAgent, prompt, label);
453
+ } catch (injectError) {
454
+ if (channel === "completion") completionInjected.delete(entrySession);
455
+ ctx.logger.warn(`dsh-evolution-review: deferred review inject failed: ${injectError instanceof Error ? injectError.message : String(injectError)}`);
456
+ continue;
457
+ }
458
+ try {
459
+ ctx.emit("evolution/review-scheduled", {
460
+ sessionId: entrySession,
461
+ kind: waitingKind,
462
+ toolCalls: entryCounts.toolCalls,
463
+ userChars: entryCounts.userChars,
464
+ assistantChars: entryCounts.assistantChars,
465
+ channel
466
+ });
467
+ } catch (emitError) {
468
+ ctx.logger.warn(`dsh-evolution-review: review-scheduled emit failed: ${emitError instanceof Error ? emitError.message : String(emitError)}`);
469
+ }
386
470
  }
387
471
  }
388
472
  }
@@ -559,6 +643,7 @@ function apply(ctx, rawConfig = {}) {
559
643
  pendingCadenceWarned.clear();
560
644
  skipNextCadenceFire.clear();
561
645
  cadenceResetWarned.clear();
646
+ reviewStateLocks.clear();
562
647
  }, "dsh-evolution-review.cleanup");
563
648
  }
564
649
  /** Completion-channel decision: task finished normally AND the session is proven long. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-review",
3
3
  "description": "Background review orchestration (community build)",
4
- "version": "0.3.66",
4
+ "version": "0.3.67",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -31,9 +31,9 @@
31
31
  "license": "MIT",
32
32
  "dependencies": {
33
33
  "@deepseek-ai/schemastery": "^3.18.1",
34
- "@lmzhen/dsh-evolution-approval": "^0.3.66",
35
- "@lmzhen/dsh-evolution-core": "^0.3.66",
36
- "@lmzhen/dsh-evolution-plan-validator": "^0.3.66"
34
+ "@lmzhen/dsh-evolution-approval": "^0.3.67",
35
+ "@lmzhen/dsh-evolution-core": "^0.3.67",
36
+ "@lmzhen/dsh-evolution-plan-validator": "^0.3.67"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@deepseek-ai/cordis": "^4.0.1",
@@ -42,8 +42,8 @@
42
42
  "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
43
43
  "@deepseek-ai/dsh-session": "^0.1.1-rc.2",
44
44
  "@deepseek-ai/dsh-tools": "^0.1.1-rc.2",
45
- "@lmzhen/dsh-evolution-state": "^0.3.66",
46
- "@lmzhen/dsh-evolution-policy": "^0.3.66"
45
+ "@lmzhen/dsh-evolution-state": "^0.3.67",
46
+ "@lmzhen/dsh-evolution-policy": "^0.3.67"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@deepseek-ai/dsh-agent": "^0.1.1-rc.2",
@@ -54,9 +54,9 @@
54
54
  "@deepseek-ai/dsh-session-persistence": "^0.1.1-rc.2",
55
55
  "@deepseek-ai/dsh-session-persistence-jsonl": "^0.1.1-rc.2",
56
56
  "@deepseek-ai/dsh-tools": "^0.1.1-rc.2",
57
- "@lmzhen/dsh-evolution-approval": "^0.3.66",
58
- "@lmzhen/dsh-evolution-core": "^0.3.66",
59
- "@lmzhen/dsh-evolution-plan-validator": "^0.3.66",
60
- "@lmzhen/dsh-evolution-state": "^0.3.66"
57
+ "@lmzhen/dsh-evolution-approval": "^0.3.67",
58
+ "@lmzhen/dsh-evolution-core": "^0.3.67",
59
+ "@lmzhen/dsh-evolution-plan-validator": "^0.3.67",
60
+ "@lmzhen/dsh-evolution-state": "^0.3.67"
61
61
  }
62
62
  }