@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.
- package/README.md +1 -1
- package/lib/index.js +108 -23
- 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
|
|
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
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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 :
|
|
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
|
|
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
|
-
|
|
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)
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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.
|
|
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.
|
|
35
|
-
"@lmzhen/dsh-evolution-core": "^0.3.
|
|
36
|
-
"@lmzhen/dsh-evolution-plan-validator": "^0.3.
|
|
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.
|
|
46
|
-
"@lmzhen/dsh-evolution-policy": "^0.3.
|
|
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.
|
|
58
|
-
"@lmzhen/dsh-evolution-core": "^0.3.
|
|
59
|
-
"@lmzhen/dsh-evolution-plan-validator": "^0.3.
|
|
60
|
-
"@lmzhen/dsh-evolution-state": "^0.3.
|
|
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
|
}
|