@lmzhen/dsh-evolution-review 0.17.1 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/lib/index.js +110 -124
- package/lib/types/index.d.ts +2 -2
- package/lib/types/review-notice.d.ts +8 -0
- package/package.json +12 -12
package/README.md
CHANGED
|
@@ -13,6 +13,7 @@ own, and the delivery mode lives on the `evolution-policy` row, not here.
|
|
|
13
13
|
|
|
14
14
|
## Known limitations
|
|
15
15
|
|
|
16
|
+
- **The autonomous write leg does not pass `tools.guard`.** `executePlan` calls the service seam directly (`memory.applyBatch`, `library.*`), so the guard `evolution-threat` installs on the TOOL channel never sees these writes (`grep "tools.execute(" src` = 0 by design). The scan still happens one layer down: `MemoryStore` and `SkillLibrary` refuse threat-shaped payloads at the write point through core's own pattern table, which is the same table the guard consumes. A deployment that wants the tool channel's guard wording on a plan write should route it through a tool — nothing in the plan path claims that coverage.
|
|
16
17
|
- The review's model/context routing below applies to `reviewMode: 'subagent'` (opt-in since 0.3.74); in the default `'inject'` mode the review runs on the parent agent's model inside the parent thread.
|
|
17
18
|
- 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
19
|
- 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.
|
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, PROMPT_BUNDLE, advanceReview, assertSkillsRootAliasRetired,
|
|
5
|
+
import { COMPLETION_SKILL_REVIEW_PROMPT, DEFAULT_MAX_OPS_PER_PLAN, DEFAULT_MEMORY_CHAR_LIMIT, DEFAULT_MEMORY_REVIEW_MODEL, DEFAULT_REVIEW_CONTEXT_MESSAGES, DEFAULT_REVIEW_MEMORY_INTERVAL, DEFAULT_REVIEW_MESSAGE_CHARS, DEFAULT_REVIEW_SKILL_INTERVAL, DEFAULT_REVIEW_TIMEOUT_MS, DEFAULT_SKILL_CONTENT_CHARS, DEFAULT_SKILL_LIMITS, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, DEFAULT_SKILL_REVIEW_MODEL, DEFAULT_SKILL_REVIEW_TRIGGER, DEFAULT_SUBSTANTIVE_MIN_AGENT_CHARS, DEFAULT_SUBSTANTIVE_MIN_TOOL_CALLS, DEFAULT_SUBSTANTIVE_MIN_USER_CHARS, DEFAULT_USER_CHAR_LIMIT, MAX_TIMER_DELAY_MS, PROMPT_BUNDLE, advanceReview, assertSkillsRootAliasRetired, clampOnce, clearReviewChannel, contentHash, evolutionIoAdapter, filterUnreadSkillOps, filterUnreadSkillOps as filterUnreadSkillOps$1, foldUntrustedLines, markReviewChannel, newSkillLibrary, paramRowId, payloadText, pickWithPolicy, policyStageLimits, readDispatchSignal, redactSecrets, resolveOrigins, resolveRootConfig, reviewPrompt, sessionAudited, sessionEvidenceIndex, sessionReadNames, sessionTurnSignals, sweepReviewChannelSessions, userSetKeys, verifyPromptBundle } from "@lmzhen/dsh-evolution-core";
|
|
6
6
|
import { validateEvolutionPlan } from "@lmzhen/dsh-evolution-plan-validator";
|
|
7
7
|
/**
|
|
8
8
|
* Whether a message is a notice this plugin delivered, judged only by durable source fields.
|
|
@@ -15,7 +15,8 @@ import { validateEvolutionPlan } from "@lmzhen/dsh-evolution-plan-validator";
|
|
|
15
15
|
function isReviewNotice(message) {
|
|
16
16
|
const source = message.source;
|
|
17
17
|
if (source?.form !== "notice") return false;
|
|
18
|
-
|
|
18
|
+
if (source.kind !== "evolution-review" && source.kind !== "plugin:dsh-evolution-review") return false;
|
|
19
|
+
return source.summary === void 0 || !source.summary.startsWith("self-improvement review");
|
|
19
20
|
}
|
|
20
21
|
/**
|
|
21
22
|
* Fold one inbox event into the session's outstanding notice.
|
|
@@ -115,6 +116,23 @@ var SessionScopedState = class {
|
|
|
115
116
|
* Background review orchestration: signal gate → one-shot subagent → trusted plan execution.
|
|
116
117
|
* @module @lmzhen/dsh-evolution-review
|
|
117
118
|
*/
|
|
119
|
+
/** Emit one `evolution/review-scheduled` inside its OWN protection domain (V8-03, V5-19③/F-334).
|
|
120
|
+
*
|
|
121
|
+
* Every delivery path must enter that domain, and five hand-copied try/emit/catch blocks had
|
|
122
|
+
* already drifted twice: V24-15 had to add the emit to two injection paths that never had it, and
|
|
123
|
+
* V25-02 fixed a cross-session attribution bug that lived in one of the copies. The fields, the
|
|
124
|
+
* channel and the domain now travel together.
|
|
125
|
+
* @param ctx - the plugin context the event is emitted on.
|
|
126
|
+
* @param event - the payload. `sessionId` is the session the review was scheduled FOR — on the
|
|
127
|
+
* deferred drain that is the entry's session, never the in-flight review's.
|
|
128
|
+
*/
|
|
129
|
+
function emitReviewScheduled(ctx, event) {
|
|
130
|
+
try {
|
|
131
|
+
ctx.emit("evolution/review-scheduled", event);
|
|
132
|
+
} catch (emitError) {
|
|
133
|
+
ctx.logger.warn(`dsh-evolution-review: review-scheduled emit failed: ${emitError instanceof Error ? emitError.message : String(emitError)}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
118
136
|
/** S2-6 (FLOW1-1): the settle-grace margin a subagent handle gets beyond its
|
|
119
137
|
* own deadline before the review abandons it. As the dispose watchdog's WHOLE
|
|
120
138
|
* budget it is capped by the review timeout so a short (test) budget stays
|
|
@@ -176,37 +194,6 @@ const REVIEW_OUTPUT_SCHEMA = {
|
|
|
176
194
|
*/
|
|
177
195
|
const cadenceSummary = (kind) => `auto-review:${kind}`;
|
|
178
196
|
/**
|
|
179
|
-
* G3.1 (0.3.23): clamp one numeric review knob so a 0/negative/NaN/±Infinity value
|
|
180
|
-
* falls back to the package default instead of folding as a "disabled" special value
|
|
181
|
-
* (a 0 interval would fire a review every turn; NaN folds as NaN into the
|
|
182
|
-
* cadence/timeout). The schema `.min(1)` guards the loader path; this clamp also
|
|
183
|
-
* covers NaN/±Infinity (which schemastery lets a bare number schema through) and
|
|
184
|
-
* programmatic assembly. `reviewMaxDepth` clamps to at least 1 because 0 is the
|
|
185
|
-
* historical 0.3.1 maximum-depth defect (a 0 rejects the spawn outright). The warning
|
|
186
|
-
* fires once per key: a live field is read at every use, so an assembly-time list
|
|
187
|
-
* would repeat on each read.
|
|
188
|
-
* @param warned - the keys this caller already warned about.
|
|
189
|
-
* @param warn - the warning sink.
|
|
190
|
-
* @param name - the config key, for the warning.
|
|
191
|
-
* @param value - the value as supplied, or its volatile reference.
|
|
192
|
-
* @param fallback - the package default a corrected value falls back to.
|
|
193
|
-
* @param min - the smallest value the engine can act on.
|
|
194
|
-
* @param max - the largest value the carrier can hold, when bounded.
|
|
195
|
-
* @returns the value the engine may use.
|
|
196
|
-
*/
|
|
197
|
-
function clampField(warned, warn, name, value, fallback, min, max) {
|
|
198
|
-
const current = typeof value === "object" ? value.get() : value;
|
|
199
|
-
const result = clampedNumber(current, fallback, max === void 0 ? { min } : {
|
|
200
|
-
min,
|
|
201
|
-
max
|
|
202
|
-
});
|
|
203
|
-
if (current !== void 0 && result !== current && !warned.has(name)) {
|
|
204
|
-
warned.add(name);
|
|
205
|
-
warn(`${name} provided an invalid value; falling back to the default`);
|
|
206
|
-
}
|
|
207
|
-
return result;
|
|
208
|
-
}
|
|
209
|
-
/**
|
|
210
197
|
* The DEPLOYMENT layer of this row, clamped once at assembly: the restart-only
|
|
211
198
|
* numerics (registry tier E2) whose consumers read them from this snapshot. The
|
|
212
199
|
* seven E3 keys are NOT resolved here — they are live (G1 §8.3), and
|
|
@@ -220,7 +207,7 @@ function clampReviewConfig(rawConfig, ctx) {
|
|
|
220
207
|
const warn = (message) => {
|
|
221
208
|
ctx.logger.warn("dsh-evolution-review: " + message);
|
|
222
209
|
};
|
|
223
|
-
const field = (name, value, fallback, min, max) =>
|
|
210
|
+
const field = (name, value, fallback, min, max) => clampOnce(warned, warn, name, value, fallback, min, max);
|
|
224
211
|
return Object.assign({}, rawConfig, {
|
|
225
212
|
reviewTimeoutMs: field("reviewTimeoutMs", rawConfig.reviewTimeoutMs, DEFAULT_REVIEW_TIMEOUT_MS, 1, MAX_TIMER_DELAY_MS),
|
|
226
213
|
reviewContextMessages: field("reviewContextMessages", rawConfig.reviewContextMessages, DEFAULT_REVIEW_CONTEXT_MESSAGES, 1),
|
|
@@ -244,10 +231,10 @@ function policySnapshotOf(source) {
|
|
|
244
231
|
function resolveReviewRowSettings(rawConfig, warn) {
|
|
245
232
|
const warned = /* @__PURE__ */ new Set();
|
|
246
233
|
return {
|
|
247
|
-
reviewSkillInterval:
|
|
248
|
-
reviewMemoryInterval:
|
|
234
|
+
reviewSkillInterval: clampOnce(warned, warn, "reviewSkillInterval", rawConfig.reviewSkillInterval?.get() ?? rawConfig.skillInterval, DEFAULT_REVIEW_SKILL_INTERVAL, 1),
|
|
235
|
+
reviewMemoryInterval: clampOnce(warned, warn, "reviewMemoryInterval", rawConfig.reviewMemoryInterval?.get() ?? rawConfig.memoryInterval, DEFAULT_REVIEW_MEMORY_INTERVAL, 1),
|
|
249
236
|
skillReviewTrigger: rawConfig.skillReviewTrigger?.get() ?? DEFAULT_SKILL_REVIEW_TRIGGER,
|
|
250
|
-
skillReviewCompletionMinToolCalls:
|
|
237
|
+
skillReviewCompletionMinToolCalls: clampOnce(warned, warn, "skillReviewCompletionMinToolCalls", rawConfig.skillReviewCompletionMinToolCalls, DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS, 1),
|
|
251
238
|
reviewEnabled: rawConfig.reviewEnabled?.get() ?? true,
|
|
252
239
|
reviewMode: rawConfig.reviewMode?.get() ?? "inject",
|
|
253
240
|
reviewWakeInject: rawConfig.reviewWakeInject?.get() ?? true
|
|
@@ -257,7 +244,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
257
244
|
ctx.inject(["settings"], (injected) => {
|
|
258
245
|
const settings = injected.settings;
|
|
259
246
|
if (typeof settings?.configure !== "function") return;
|
|
260
|
-
const disposer = settings.configure({ auto: false });
|
|
247
|
+
const disposer = settings.configure({ auto: false }, ctx.fiber);
|
|
261
248
|
if (typeof disposer === "function") ctx.effect(() => disposer, "dsh-evolution-review: settings presentation");
|
|
262
249
|
});
|
|
263
250
|
if (!verifyPromptBundle(PROMPT_BUNDLE)) throw new Error("dsh-evolution prompt bundle integrity check failed; refusing to schedule review work");
|
|
@@ -285,25 +272,11 @@ function apply(ctx, rawConfig = {}) {
|
|
|
285
272
|
const rowSettings = () => resolveReviewRowSettings(rawConfig, (message) => {
|
|
286
273
|
ctx.logger.warn("dsh-evolution-review: " + message);
|
|
287
274
|
});
|
|
288
|
-
/**
|
|
289
|
-
* The keys the user set in this row's namespace (G1 §8.4-A). This is the ONLY
|
|
290
|
-
* remaining read of the settings user layer, and it reads KEY NAMES, never values:
|
|
291
|
-
* the platform resolves the user layer into the row's live fields, so the value
|
|
292
|
-
* comes from `rowSettings()`.
|
|
293
|
-
* @returns the keys present in this row's user layer.
|
|
294
|
-
*/
|
|
295
|
-
const userSetKeys = () => {
|
|
296
|
-
const entry = ctx.get("settings")?.describe?.({ redactSecrets: false }).find((item) => item.ns === paramRowId("evolution-review"));
|
|
297
|
-
return new Set(Object.keys(entry?.user ?? {}));
|
|
298
|
-
};
|
|
299
275
|
const params = () => {
|
|
300
276
|
const row = rowSettings();
|
|
301
|
-
const userSet = userSetKeys();
|
|
277
|
+
const userSet = userSetKeys(ctx, paramRowId("evolution-review"));
|
|
302
278
|
const snapshot = policy();
|
|
303
|
-
const pick = (key) =>
|
|
304
|
-
if (userSet.has(key)) return row[key];
|
|
305
|
-
return snapshot?.[key] ?? row[key];
|
|
306
|
-
};
|
|
279
|
+
const pick = (key) => pickWithPolicy(userSet, row, snapshot, key);
|
|
307
280
|
const mode = () => {
|
|
308
281
|
if (userSet.has("reviewMode")) return row.reviewMode;
|
|
309
282
|
return snapshot?.reviewMode ?? row.reviewMode;
|
|
@@ -335,7 +308,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
335
308
|
["reviewMode", void 0],
|
|
336
309
|
["reviewMemoryInterval", "memoryInterval"],
|
|
337
310
|
["reviewSkillInterval", "skillInterval"]
|
|
338
|
-
].filter(([id, alias]) => !userSetKeys().has(id) && rowSupplied(id, alias)).map(([id]) => id);
|
|
311
|
+
].filter(([id, alias]) => !userSetKeys(ctx, paramRowId("evolution-review")).has(id) && rowSupplied(id, alias)).map(([id]) => id);
|
|
339
312
|
if (shadowedRowFields.length > 0) {
|
|
340
313
|
let shadowWarned = false;
|
|
341
314
|
const warnShadowed = () => {
|
|
@@ -367,7 +340,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
367
340
|
return;
|
|
368
341
|
}
|
|
369
342
|
if (event.type !== "turn/end") return;
|
|
370
|
-
if (cumulativeToolCalls.size >= COUNTER_SWEEP_THRESHOLD || dispatchDigest.size >= COUNTER_SWEEP_THRESHOLD ||
|
|
343
|
+
if (cumulativeToolCalls.size >= COUNTER_SWEEP_THRESHOLD || dispatchDigest.size >= COUNTER_SWEEP_THRESHOLD || completionInjected.size >= COUNTER_SWEEP_THRESHOLD || pendingCadenceReviews.size >= COUNTER_SWEEP_THRESHOLD || pendingCadenceWarned.size >= COUNTER_SWEEP_THRESHOLD || cadenceResetWarned.size >= COUNTER_SWEEP_THRESHOLD || pendingReviewNotices.size >= COUNTER_SWEEP_THRESHOLD) {
|
|
371
344
|
const isAlive = (id) => ctx.agents.get(id) !== void 0;
|
|
372
345
|
sweepDeadSessionEntries(cumulativeToolCalls, isAlive);
|
|
373
346
|
sweepDeadSessionEntries(dispatchDigest, isAlive);
|
|
@@ -503,33 +476,25 @@ function apply(ctx, rawConfig = {}) {
|
|
|
503
476
|
pendingCadenceReviews.set(session.id, pendingKind);
|
|
504
477
|
return;
|
|
505
478
|
}
|
|
506
|
-
|
|
507
|
-
|
|
479
|
+
emitReviewScheduled(ctx, {
|
|
480
|
+
sessionId: session.id,
|
|
481
|
+
kind: pendingKind,
|
|
482
|
+
toolCalls: signal.toolCalls,
|
|
483
|
+
userChars: signal.userChars,
|
|
484
|
+
assistantChars: signal.assistantChars,
|
|
485
|
+
channel: "inject"
|
|
486
|
+
});
|
|
487
|
+
} else {
|
|
488
|
+
const reviewOutcome = await trySubagentReview(session, agent, pendingKind, signal);
|
|
489
|
+
if (reviewOutcome === true) {
|
|
490
|
+
emitReviewScheduled(ctx, {
|
|
508
491
|
sessionId: session.id,
|
|
509
492
|
kind: pendingKind,
|
|
510
493
|
toolCalls: signal.toolCalls,
|
|
511
494
|
userChars: signal.userChars,
|
|
512
495
|
assistantChars: signal.assistantChars,
|
|
513
|
-
channel: "
|
|
496
|
+
channel: "subagent"
|
|
514
497
|
});
|
|
515
|
-
} catch (emitError) {
|
|
516
|
-
ctx.logger.warn(`dsh-evolution-review: review-scheduled emit failed: ${emitError instanceof Error ? emitError.message : String(emitError)}`);
|
|
517
|
-
}
|
|
518
|
-
} else {
|
|
519
|
-
const reviewOutcome = await trySubagentReview(session, agent, pendingKind, signal);
|
|
520
|
-
if (reviewOutcome === true) {
|
|
521
|
-
try {
|
|
522
|
-
ctx.emit("evolution/review-scheduled", {
|
|
523
|
-
sessionId: session.id,
|
|
524
|
-
kind: pendingKind,
|
|
525
|
-
toolCalls: signal.toolCalls,
|
|
526
|
-
userChars: signal.userChars,
|
|
527
|
-
assistantChars: signal.assistantChars,
|
|
528
|
-
channel: "subagent"
|
|
529
|
-
});
|
|
530
|
-
} catch (emitError) {
|
|
531
|
-
ctx.logger.warn(`dsh-evolution-review: review-scheduled emit failed: ${emitError instanceof Error ? emitError.message : String(emitError)}`);
|
|
532
|
-
}
|
|
533
498
|
await settleNotice(session.id, "subagent-flush");
|
|
534
499
|
} else if (reviewOutcome === "dropped") {
|
|
535
500
|
pendingCadenceReviews.set(session.id, pendingKind);
|
|
@@ -539,18 +504,14 @@ function apply(ctx, rawConfig = {}) {
|
|
|
539
504
|
pendingCadenceReviews.set(session.id, pendingKind);
|
|
540
505
|
return;
|
|
541
506
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
});
|
|
551
|
-
} catch (emitError) {
|
|
552
|
-
ctx.logger.warn(`dsh-evolution-review: review-scheduled emit failed: ${emitError instanceof Error ? emitError.message : String(emitError)}`);
|
|
553
|
-
}
|
|
507
|
+
emitReviewScheduled(ctx, {
|
|
508
|
+
sessionId: session.id,
|
|
509
|
+
kind: pendingKind,
|
|
510
|
+
toolCalls: signal.toolCalls,
|
|
511
|
+
userChars: signal.userChars,
|
|
512
|
+
assistantChars: signal.assistantChars,
|
|
513
|
+
channel: "inject"
|
|
514
|
+
});
|
|
554
515
|
}
|
|
555
516
|
}
|
|
556
517
|
return;
|
|
@@ -590,18 +551,14 @@ function apply(ctx, rawConfig = {}) {
|
|
|
590
551
|
completionInjected.delete(session.id);
|
|
591
552
|
return;
|
|
592
553
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
});
|
|
602
|
-
} catch (emitError) {
|
|
603
|
-
ctx.logger.warn(`dsh-evolution-review: review-scheduled emit failed: ${emitError instanceof Error ? emitError.message : String(emitError)}`);
|
|
604
|
-
}
|
|
554
|
+
emitReviewScheduled(ctx, {
|
|
555
|
+
sessionId: session.id,
|
|
556
|
+
kind: "skill",
|
|
557
|
+
toolCalls: signal.toolCalls,
|
|
558
|
+
userChars: signal.userChars,
|
|
559
|
+
assistantChars: signal.assistantChars,
|
|
560
|
+
channel: "completion"
|
|
561
|
+
});
|
|
605
562
|
}
|
|
606
563
|
/** V7-03 (0.3.42): shared waking delivery — review prompts AND result
|
|
607
564
|
* notices go through the same followup-first channel (skip the woken turn's
|
|
@@ -734,6 +691,18 @@ function apply(ctx, rawConfig = {}) {
|
|
|
734
691
|
reject(error instanceof Error ? error : new Error(String(error)));
|
|
735
692
|
});
|
|
736
693
|
});
|
|
694
|
+
/**
|
|
695
|
+
* T3-09/A37: the ONE abort channel for this fiber's long work.
|
|
696
|
+
*
|
|
697
|
+
* The review's two legs (the review subagent, and the write leg that executes its plan) were
|
|
698
|
+
* bounded only by their own timeouts, so an unloaded or HMR-replaced fiber kept running — and the
|
|
699
|
+
* write leg kept WRITING — while the replacement instance knew nothing about it (its single-flight
|
|
700
|
+
* flag and min-idle window do not cover a run that no longer belongs to any live fiber). The
|
|
701
|
+
* disposer below aborts this controller; the subagent leg merges it with its own deadline through
|
|
702
|
+
* `AbortSignal.any` (the shape evolution-maintenance's scan already uses) and the write leg checks
|
|
703
|
+
* it at every op boundary — before the first op, so an aborted fiber lands nothing.
|
|
704
|
+
*/
|
|
705
|
+
const fiberAbort = new AbortController();
|
|
737
706
|
const withTimeout = (promise, ms, label) => new Promise((resolve, reject) => {
|
|
738
707
|
const timer = setTimeout(() => {
|
|
739
708
|
reject(/* @__PURE__ */ new Error(`dsh-evolution-review: ${label} timed out after ${ms}ms`));
|
|
@@ -818,7 +787,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
818
787
|
text: reviewText
|
|
819
788
|
}],
|
|
820
789
|
parent: agent,
|
|
821
|
-
signal: AbortSignal.timeout(config.reviewTimeoutMs),
|
|
790
|
+
signal: AbortSignal.any([AbortSignal.timeout(config.reviewTimeoutMs), fiberAbort.signal]),
|
|
822
791
|
maxDepth: config.reviewMaxDepth,
|
|
823
792
|
agentOptions,
|
|
824
793
|
persona: reviewPrompt(kind, "plan"),
|
|
@@ -887,7 +856,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
887
856
|
};
|
|
888
857
|
let executed;
|
|
889
858
|
try {
|
|
890
|
-
executed = await withTimeout(executePlan(validation.accepted, session, noteLanded, preRunHashes), config.reviewTimeoutMs, "review plan execution");
|
|
859
|
+
executed = await withTimeout(executePlan(validation.accepted, session, noteLanded, preRunHashes, fiberAbort.signal), config.reviewTimeoutMs, "review plan execution");
|
|
891
860
|
} catch (error) {
|
|
892
861
|
deadlineMissed = true;
|
|
893
862
|
emitApplied({
|
|
@@ -943,22 +912,18 @@ function apply(ctx, rawConfig = {}) {
|
|
|
943
912
|
else pendingCadenceReviews.set(entrySession, waitingKind);
|
|
944
913
|
continue;
|
|
945
914
|
}
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
});
|
|
955
|
-
} catch (emitError) {
|
|
956
|
-
ctx.logger.warn(`dsh-evolution-review: review-scheduled emit failed: ${emitError instanceof Error ? emitError.message : String(emitError)}`);
|
|
957
|
-
}
|
|
915
|
+
emitReviewScheduled(ctx, {
|
|
916
|
+
sessionId: entrySession,
|
|
917
|
+
kind: waitingKind,
|
|
918
|
+
toolCalls: entryCounts.toolCalls,
|
|
919
|
+
userChars: entryCounts.userChars,
|
|
920
|
+
assistantChars: entryCounts.assistantChars,
|
|
921
|
+
channel
|
|
922
|
+
});
|
|
958
923
|
}
|
|
959
924
|
}
|
|
960
925
|
}
|
|
961
|
-
async function executePlan(plan, session, onLanded, preRunHashes) {
|
|
926
|
+
async function executePlan(plan, session, onLanded, preRunHashes, signal) {
|
|
962
927
|
const sessionId = session?.id;
|
|
963
928
|
const memory = ctx.get("memory");
|
|
964
929
|
const approval = ctx.get("evolutionApproval");
|
|
@@ -976,6 +941,16 @@ function apply(ctx, rawConfig = {}) {
|
|
|
976
941
|
let ok = true;
|
|
977
942
|
try {
|
|
978
943
|
for (const op of plan.memoryOps ?? []) {
|
|
944
|
+
if (signal?.aborted === true) {
|
|
945
|
+
const reason = "the review was unloaded mid-plan";
|
|
946
|
+
ctx.logger.warn(`dsh-evolution-review: executePlan stopped at an op boundary after ${actions.length} op(s): ${reason}`);
|
|
947
|
+
return {
|
|
948
|
+
actions,
|
|
949
|
+
ok: false,
|
|
950
|
+
failedOps,
|
|
951
|
+
aborted: reason
|
|
952
|
+
};
|
|
953
|
+
}
|
|
979
954
|
if (!Array.isArray(op.evidence) || op.evidence.length === 0) {
|
|
980
955
|
ok = false;
|
|
981
956
|
failedOps.push(`memory ${op.action ?? "add"} ${op.target}: missing evidence (defense-in-depth rejection)`);
|
|
@@ -984,7 +959,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
984
959
|
const normalized = {
|
|
985
960
|
target: op.target === "user" ? "user" : "memory",
|
|
986
961
|
action: op.action ?? "add",
|
|
987
|
-
facts: op
|
|
962
|
+
facts: payloadText(op),
|
|
988
963
|
old_text: op.old_text
|
|
989
964
|
};
|
|
990
965
|
const result = approval ? await runApproved("memory", `memory ${normalized.target} ${normalized.action}`, normalized, normalized, session) : await memory?.applyBatch(normalized.target, [normalized]);
|
|
@@ -997,6 +972,16 @@ function apply(ctx, rawConfig = {}) {
|
|
|
997
972
|
}
|
|
998
973
|
}
|
|
999
974
|
for (const op of plan.skillOps ?? []) {
|
|
975
|
+
if (signal?.aborted === true) {
|
|
976
|
+
const reason = "the review was unloaded mid-plan";
|
|
977
|
+
ctx.logger.warn(`dsh-evolution-review: executePlan stopped at an op boundary after ${actions.length} op(s): ${reason}`);
|
|
978
|
+
return {
|
|
979
|
+
actions,
|
|
980
|
+
ok: false,
|
|
981
|
+
failedOps,
|
|
982
|
+
aborted: reason
|
|
983
|
+
};
|
|
984
|
+
}
|
|
1000
985
|
if (!Array.isArray(op.evidence) || op.evidence.length === 0 || !op.name) {
|
|
1001
986
|
ok = false;
|
|
1002
987
|
failedOps.push(`skill ${op.action ?? "patch"} ${op.name ?? "<unnamed>"}: missing evidence or name (defense-in-depth rejection)`);
|
|
@@ -1093,7 +1078,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1093
1078
|
if (!memory?.applyBatch) return void 0;
|
|
1094
1079
|
return await memory.applyBatch(op.target === "user" ? "user" : "memory", [{
|
|
1095
1080
|
action: op.action ?? "add",
|
|
1096
|
-
facts: op
|
|
1081
|
+
facts: payloadText(op),
|
|
1097
1082
|
old_text: op.old_text
|
|
1098
1083
|
}]);
|
|
1099
1084
|
}
|
|
@@ -1185,6 +1170,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
1185
1170
|
}
|
|
1186
1171
|
}
|
|
1187
1172
|
ctx.effect(() => () => {
|
|
1173
|
+
fiberAbort.abort();
|
|
1188
1174
|
sessionState.dispose();
|
|
1189
1175
|
}, "dsh-evolution-review.cleanup");
|
|
1190
1176
|
}
|
|
@@ -1340,8 +1326,8 @@ function renderToolResultLine(data, identity) {
|
|
|
1340
1326
|
output = "";
|
|
1341
1327
|
failed = false;
|
|
1342
1328
|
}
|
|
1343
|
-
const head = identity === void 0 ? "" : `${identity.name} ${identity.argsRaw
|
|
1344
|
-
return `[result]${failed ? " [ERROR]" : ""} ${head}${output
|
|
1329
|
+
const head = identity === void 0 ? "" : `${foldUntrustedLines(identity.name, 0)} ${foldUntrustedLines(identity.argsRaw, 200)} → `;
|
|
1330
|
+
return `[result]${failed ? " [ERROR]" : ""} ${head}${foldUntrustedLines(output, 500)}`;
|
|
1345
1331
|
}
|
|
1346
1332
|
/** Text of a PTC settle `content` payload (the logged ContentBlock list):
|
|
1347
1333
|
* text blocks and plain strings joined, anything else skipped. */
|
|
@@ -1375,10 +1361,10 @@ function textOfPersistedBlock(block) {
|
|
|
1375
1361
|
}
|
|
1376
1362
|
function buildReviewRequest(session, kind, signal, maxMessages, maxMessageChars, dispatchTail = []) {
|
|
1377
1363
|
const messages = [];
|
|
1378
|
-
const
|
|
1379
|
-
for (const message of
|
|
1364
|
+
const eligible = session.deriveMessages().filter((message) => message.role === "user" || message.role === "assistant").slice(-maxMessages);
|
|
1365
|
+
for (const message of eligible) {
|
|
1380
1366
|
const text = message.content.map(textOfPersistedBlock).join(" ").trim();
|
|
1381
|
-
if (text) messages.push(`${message.role.toUpperCase()}: ${text
|
|
1367
|
+
if (text) messages.push(`${message.role.toUpperCase()}: ${foldUntrustedLines(text, maxMessageChars)}`);
|
|
1382
1368
|
}
|
|
1383
1369
|
const toolLines = [];
|
|
1384
1370
|
const events = dispatchTail;
|
|
@@ -1412,7 +1398,7 @@ function buildReviewRequest(session, kind, signal, maxMessages, maxMessageChars,
|
|
|
1412
1398
|
if (opened === null || openedCallIds.has(opened.callId)) continue;
|
|
1413
1399
|
openedCallIds.add(opened.callId);
|
|
1414
1400
|
const argsRaw = typeof opened.arguments === "string" ? opened.arguments : JSON.stringify(opened.arguments ?? {});
|
|
1415
|
-
toolLines.push(`[call] ${opened.name} ${argsRaw
|
|
1401
|
+
toolLines.push(`[call] ${foldUntrustedLines(opened.name, 0)} ${foldUntrustedLines(argsRaw, 500)}`);
|
|
1416
1402
|
}
|
|
1417
1403
|
toolLines.reverse();
|
|
1418
1404
|
return [
|
package/lib/types/index.d.ts
CHANGED
|
@@ -42,10 +42,10 @@ export interface Config {
|
|
|
42
42
|
* the policy snapshot like the memory cadence. */
|
|
43
43
|
reviewSkillInterval?: Volatile<number | undefined>;
|
|
44
44
|
/** Deprecated alias of `reviewMemoryInterval` (G0/S0.3): still readable,
|
|
45
|
-
* refused by writes;
|
|
45
|
+
* refused by writes; removal was planned for 0.7.0 and is deferred (the alias still ships — see the package README). */
|
|
46
46
|
memoryInterval?: number;
|
|
47
47
|
/** Deprecated alias of `reviewSkillInterval` (G0/S0.3): still readable,
|
|
48
|
-
* refused by writes;
|
|
48
|
+
* refused by writes; removal was planned for 0.7.0 and is deferred (the alias still ships — see the package README). */
|
|
49
49
|
skillInterval?: number;
|
|
50
50
|
/**
|
|
51
51
|
* Tools the one-shot review subagent may use. Only actually-existing tools
|
|
@@ -31,12 +31,20 @@ export declare const REVIEW_NOTICE_KIND = "evolution-review";
|
|
|
31
31
|
* belonged to would be delivered a second time.
|
|
32
32
|
*/
|
|
33
33
|
export declare const REVIEW_NOTICE_LEGACY_KIND = "plugin:dsh-evolution-review";
|
|
34
|
+
/** T3-01/A29: the summary every RESULT notification carries ("what the review did"). A result notice
|
|
35
|
+
* is NOT a review prompt: it occupies no window and must never refuse the next cadence flush — but it
|
|
36
|
+
* shares the kind AND the form (the platform types `form` as the literal 'notice'), so this summary is
|
|
37
|
+
* the only field that tells the two apart, for new notices and for the ones an older build persisted. */
|
|
38
|
+
export declare const REVIEW_RESULT_SUMMARY = "self-improvement review";
|
|
34
39
|
/** The subset of a platform message this module reads. */
|
|
35
40
|
export interface NoticeMessage {
|
|
36
41
|
readonly id: string;
|
|
37
42
|
readonly source?: {
|
|
38
43
|
readonly kind?: string;
|
|
39
44
|
readonly form?: string;
|
|
45
|
+
/** Present on both shapes this plugin delivers (a prompt and a result notice); the result
|
|
46
|
+
* summary is what identifies a LEGACY result notice, see {@link REVIEW_RESULT_SUMMARY}. */
|
|
47
|
+
readonly summary?: string;
|
|
40
48
|
} | undefined;
|
|
41
49
|
}
|
|
42
50
|
/** One delivered notice that has not settled yet. */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lmzhen/dsh-evolution-review",
|
|
3
3
|
"description": "Background review orchestration (community build)",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.18.1",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -26,10 +26,10 @@
|
|
|
26
26
|
],
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@deepseek-ai/schemastery": "^3.18.
|
|
30
|
-
"@lmzhen/dsh-evolution-approval": "^0.
|
|
31
|
-
"@lmzhen/dsh-evolution-core": "^0.
|
|
32
|
-
"@lmzhen/dsh-evolution-plan-validator": "^0.
|
|
29
|
+
"@deepseek-ai/schemastery": "^3.18.4",
|
|
30
|
+
"@lmzhen/dsh-evolution-approval": "^0.18.1",
|
|
31
|
+
"@lmzhen/dsh-evolution-core": "^0.18.1",
|
|
32
|
+
"@lmzhen/dsh-evolution-plan-validator": "^0.18.1"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"@deepseek-ai/dsh-llm": "^0.2.0-rc.1",
|
|
38
38
|
"@deepseek-ai/dsh-session": "^0.2.0-rc.1",
|
|
39
39
|
"@deepseek-ai/dsh-tools": "^0.2.0-rc.1",
|
|
40
|
-
"@lmzhen/dsh-evolution-state": "^0.
|
|
41
|
-
"@lmzhen/dsh-evolution-policy": "^0.
|
|
40
|
+
"@lmzhen/dsh-evolution-state": "^0.18.1",
|
|
41
|
+
"@lmzhen/dsh-evolution-policy": "^0.18.1"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@deepseek-ai/dsh-agent": "^0.2.0-rc.1",
|
|
@@ -48,10 +48,10 @@
|
|
|
48
48
|
"@deepseek-ai/dsh-session-persistence": "^0.2.0-rc.1",
|
|
49
49
|
"@deepseek-ai/dsh-session-persistence-jsonl": "^0.2.0-rc.1",
|
|
50
50
|
"@deepseek-ai/dsh-tools": "^0.2.0-rc.1",
|
|
51
|
-
"@lmzhen/dsh-evolution-approval": "^0.
|
|
52
|
-
"@lmzhen/dsh-evolution-core": "^0.
|
|
53
|
-
"@lmzhen/dsh-evolution-curator": "^0.
|
|
54
|
-
"@lmzhen/dsh-evolution-plan-validator": "^0.
|
|
55
|
-
"@lmzhen/dsh-evolution-state": "^0.
|
|
51
|
+
"@lmzhen/dsh-evolution-approval": "^0.18.1",
|
|
52
|
+
"@lmzhen/dsh-evolution-core": "^0.18.1",
|
|
53
|
+
"@lmzhen/dsh-evolution-curator": "^0.18.1",
|
|
54
|
+
"@lmzhen/dsh-evolution-plan-validator": "^0.18.1",
|
|
55
|
+
"@lmzhen/dsh-evolution-state": "^0.18.1"
|
|
56
56
|
}
|
|
57
57
|
}
|