@lmzhen/dsh-evolution-review 0.3.74 → 0.3.76
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 +5 -1
- package/lib/index.js +116 -53
- package/lib/types/index.d.ts +6 -3
- package/package.json +11 -17
- package/lib/invariant.js +0 -8
- package/lib/types/invariant.d.ts +0 -5
package/README.md
CHANGED
|
@@ -30,6 +30,8 @@ Independent of request-prefix construction. This package does not alter the asse
|
|
|
30
30
|
- `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`.
|
|
31
31
|
- 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.
|
|
32
32
|
- 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).
|
|
33
|
+
- **The default `reviewMode: 'inject'` produces no plan ledger.** The review runs in the parent session and emits no `evolution/plan-applied` event, so `evolution-activity`'s `activity.json` and the `evolution-replay` leaderboard never grow in this mode — the only production emit point sits inside the subagent path. Set `reviewMode: 'subagent'` on the `evolution-policy` row when the plan audit trail is required; the plugin states this once at load instead of leaving an empty ledger to be read as "no reviews happened" (v37 S2.2, plan decision (c)).
|
|
34
|
+
- **`.pinned` protection in the `'inject'` channel rides a family-internal session mark.** The review prompt marks the parent session, `tool-skill-manage` reads that mark and resolves both origin surfaces (approval + library) to `background_review`, and the next REAL user message (`source.kind === 'user'`) clears it — plugin notices, including the review prompt itself, do not. Two bounds follow from the platform's inject contract (no driver wake; a prompt is dropped on cancel/dispose and may be missed with an already-claimed batch): with `reviewWakeInject: false`, or on a host without `followup`, (a) a pending prompt may never execute while the session stays idle, and (b) when the user's next message wakes the session, the mark is cleared before that pending prompt reaches the model — its writes are then attributed `foreground` and the pinned guard does not cover them. The mark is in-memory, so a restart drops the window (v37 S2.2, plan decision (b)).
|
|
33
35
|
|
|
34
36
|
## Configuration
|
|
35
37
|
|
|
@@ -41,5 +43,7 @@ Independent of request-prefix construction. This package does not alter the asse
|
|
|
41
43
|
|
|
42
44
|
- **Both channels execute at conversation end only** (a `turn/end` with `reason.kind === 'completed'`): a cadence threshold fire mid-task merely latches the kind — no subagent spawn, no inject. The flush runs BEFORE the latch block (the completing turn may itself be a threshold-firing turn). `reviewMode` selects how the flush delivers: **`'inject'` is the default since 0.3.74**, `'subagent'` is an explicit opt-in. Rationale: the parent session already holds a warm prefix cache, so an injected prompt costs the new tokens only, while a spawned child re-prefills its own system prompt plus a redacted, re-serialized conversation digest (`buildReviewRequest`) under a **different model** (`skillReviewModel`/`memoryReviewModel`) — no prefix is shared with the parent, so the whole child input is paid at full price. `'subagent'` remains the choice for deployments that want the parent context kept clean (a review's skill reads and plan do not join the parent thread) or a dedicated review model; the subagent-only knobs (`reviewProvider`, `reviewTimeoutMs`, `reviewMaxDepth`, `reviewToolAllow`, the review models) are inert in inject mode. The explicit `'inject'` mode's historical "immediate on threshold" contract was superseded in 0.3.39 — both modes are end-of-conversation.
|
|
43
45
|
- **`skillReviewTrigger`** (default `'cadence'`): the cadence channel is **always on** (one end-of-conversation review from the cadence latch per task segment); the flag gates **only the completion channel** — `'cadence'` disables it, `'completion'` enables it (cadence still fires), `'both'` enables it on top of the always-on cadence. At one boundary a turn is served by exactly one review: the cadence flush runs first and returns, so `'both'` never double-sends a second task-complete prompt at the same boundary (V10-13).
|
|
44
|
-
- **`reviewWakeInject`** (default `true`): deliveries use `agent.followup` (next-turn + wake — the model starts processing immediately) instead of the non-waking `agent.inject` (which waits for the next driver wake). The host falls back to `inject` when it has no followup or the option is `false`. **The wake primitive is always called ON the agent instance** — the platform's `Agent.followup`/`inject` are prototype methods that call `this.send(...)`, so extracting one into a local and calling the detached reference throws (0.3.73: that throw was caught and logged while the cadence reset still ran, silently consuming every segment's review from 2026-09-07). A refused delivery now returns `false` and the caller keeps its latch and counters, so the review retries at the next completed boundary instead of vanishing; `
|
|
46
|
+
- **`reviewWakeInject`** (default `true`): deliveries use `agent.followup` (next-turn + wake — the model starts processing immediately) instead of the non-waking `agent.inject` (which waits for the next driver wake). The host falls back to `inject` when it has no followup or the option is `false`. **The wake primitive is always called ON the agent instance** — the platform's `Agent.followup`/`inject` are prototype methods that call `this.send(...)`, so extracting one into a local and calling the detached reference throws (0.3.73: that throw was caught and logged while the cadence reset still ran, silently consuming every segment's review from 2026-09-07). A refused delivery now returns `false` and the caller keeps its latch and counters, so the review retries at the next completed boundary instead of vanishing; rule N13b in `packages/scripts/verify-arch-guards.mjs` pins the call form mechanically (comments and string literals are masked, and the detector self-tests at startup). The woken turn's own cadence fire is suppressed once (an injected review prompt alone must not re-trigger a review under `interval=1`); a restart clears the queue, so the loop cannot survive it.
|
|
45
47
|
- **Counting window = injection-to-injection**: the `turnsSinceMemory`/`turnsSinceSkill` counters are monotonic across threshold fires (`resetOnFire: false`) and are zeroed at the flush delivery — a continued conversation starts a fresh segment from the injection. A threshold fire on the completing turn is caught by the flush (`pendingKind = latch ?? kind`). All deliveries (review prompt AND result notices) share the same waking channel; a failed counter-reset persist warns once per session (a stateful reload may re-deliver).
|
|
48
|
+
|
|
49
|
+
**Runtime invariant:** No companion is published. The platform auto-assembles nothing and the family mounts no `<pkg>/invariant` cordis row, so a companion here would never execute (v37 S2.1 / I-3).
|
package/lib/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
3
|
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
4
|
-
import {
|
|
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, clampedNumber, clearReviewChannel, contentHash, evolutionIoAdapter, foldToolDispatches, foldTurn, markReviewChannel, newSkillLibrary, readDispatchSignal, redactSecrets, resolveOrigins, resolveRootConfig, reviewPrompt, skillReadNameOf, sweepReviewChannelSessions, verifyPromptBundle } from "@lmzhen/dsh-evolution-core";
|
|
5
6
|
import { validateEvolutionPlan } from "@lmzhen/dsh-evolution-plan-validator";
|
|
6
7
|
//#region lib/types/index.js
|
|
7
8
|
/**
|
|
@@ -92,6 +93,25 @@ function apply(ctx, rawConfig = {}) {
|
|
|
92
93
|
const cadenceResetWarned = /* @__PURE__ */ new Set();
|
|
93
94
|
let reviewInFlight = false;
|
|
94
95
|
const policy = () => ctx.get("evolutionPolicy")?.get();
|
|
96
|
+
const schemaDefaults = Config["~standard"].validate({}).value;
|
|
97
|
+
const shadowedRowFields = [
|
|
98
|
+
"reviewMode",
|
|
99
|
+
"memoryInterval",
|
|
100
|
+
"skillInterval"
|
|
101
|
+
].filter((field) => rawConfig[field] !== void 0 && rawConfig[field] !== schemaDefaults[field]);
|
|
102
|
+
if (shadowedRowFields.length > 0) {
|
|
103
|
+
let shadowWarned = false;
|
|
104
|
+
const warnShadowed = () => {
|
|
105
|
+
if (shadowWarned) return;
|
|
106
|
+
shadowWarned = true;
|
|
107
|
+
ctx.logger.warn(`dsh-evolution-review: this row sets ${shadowedRowFields.join(", ")}, but the mounted evolution-policy service overrides all three — these row values have no effect. Set them on the evolution-policy row instead.`);
|
|
108
|
+
};
|
|
109
|
+
if (policy() !== void 0) warnShadowed();
|
|
110
|
+
else ctx.inject(["evolutionPolicy"], () => {
|
|
111
|
+
warnShadowed();
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if ((policy()?.reviewMode ?? config.reviewMode) === "inject") ctx.logger.warn("dsh-evolution-review: reviewMode \"inject\" (default) runs the review in the parent session and emits NO evolution/plan-applied ledger entry — evolution-activity and evolution-replay stay empty in this mode (see packages/docs/known-limitations.md). Set reviewMode: \"subagent\" on the evolution-policy row to keep the audited plan path.");
|
|
95
115
|
const reviewStateLocks = /* @__PURE__ */ new Map();
|
|
96
116
|
async function withReviewStateLock(id, task) {
|
|
97
117
|
const next = (reviewStateLocks.get(id) ?? Promise.resolve()).catch(() => {}).then(task);
|
|
@@ -104,6 +124,10 @@ function apply(ctx, rawConfig = {}) {
|
|
|
104
124
|
}
|
|
105
125
|
ctx.on("session/event", (session, event) => {
|
|
106
126
|
if (event.type === "turn/start" && session.header.origin !== "subagent") turnStarts.set(session.id, session.seq - 1);
|
|
127
|
+
if (event.type === "user/message") {
|
|
128
|
+
if ((event.data?.source)?.kind === "user") clearReviewChannel(session.id);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
107
131
|
if (event.type !== "turn/end") return;
|
|
108
132
|
if (turnStarts.size >= COUNTER_SWEEP_THRESHOLD || cumulativeToolCalls.size >= COUNTER_SWEEP_THRESHOLD || completionInjected.size >= COUNTER_SWEEP_THRESHOLD || pendingCadenceReviews.size >= COUNTER_SWEEP_THRESHOLD || skipNextCadenceFire.size >= COUNTER_SWEEP_THRESHOLD || cadenceResetWarned.size >= COUNTER_SWEEP_THRESHOLD) {
|
|
109
133
|
const isAlive = (id) => ctx.agents.get(id) !== void 0;
|
|
@@ -114,6 +138,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
114
138
|
sweepDeadSessionEntries(pendingCadenceWarned, isAlive);
|
|
115
139
|
sweepDeadSessionEntries(skipNextCadenceFire, isAlive);
|
|
116
140
|
sweepDeadSessionEntries(cadenceResetWarned, isAlive);
|
|
141
|
+
sweepReviewChannelSessions((id) => ctx.agents.get(SessionId(id)) !== void 0);
|
|
117
142
|
}
|
|
118
143
|
onTurnEnd(session, event);
|
|
119
144
|
});
|
|
@@ -174,7 +199,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
174
199
|
if (pendingKind !== void 0) {
|
|
175
200
|
pendingCadenceReviews.delete(session.id);
|
|
176
201
|
if ((policy()?.reviewMode ?? config.reviewMode) === "inject") {
|
|
177
|
-
if (!deliverMessage(agent, reviewPrompt(pendingKind), "auto-review")) {
|
|
202
|
+
if (!deliverMessage(agent, reviewPrompt(pendingKind), "auto-review", true)) {
|
|
178
203
|
pendingCadenceReviews.set(session.id, pendingKind);
|
|
179
204
|
return;
|
|
180
205
|
}
|
|
@@ -208,7 +233,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
208
233
|
pendingCadenceReviews.set(session.id, pendingKind);
|
|
209
234
|
return;
|
|
210
235
|
} else if (reviewOutcome !== "deferred") {
|
|
211
|
-
if (!deliverMessage(agent, reviewPrompt(pendingKind), "auto-review")) {
|
|
236
|
+
if (!deliverMessage(agent, reviewPrompt(pendingKind), "auto-review", true)) {
|
|
212
237
|
pendingCadenceReviews.set(session.id, pendingKind);
|
|
213
238
|
return;
|
|
214
239
|
}
|
|
@@ -272,7 +297,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
272
297
|
}
|
|
273
298
|
return;
|
|
274
299
|
}
|
|
275
|
-
if (!deliverMessage(agent, COMPLETION_SKILL_REVIEW_PROMPT, "completion review")) {
|
|
300
|
+
if (!deliverMessage(agent, COMPLETION_SKILL_REVIEW_PROMPT, "completion review", true)) {
|
|
276
301
|
completionInjected.delete(session.id);
|
|
277
302
|
return;
|
|
278
303
|
}
|
|
@@ -302,7 +327,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
302
327
|
* prototype method), the caller's catch demoted that to a console warning,
|
|
303
328
|
* and the cadence reset ran anyway — the segment's review was consumed with
|
|
304
329
|
* nothing queued (silent no-delivery window: 2026-09-07 → 0.3.73). */
|
|
305
|
-
const deliverMessage = (agent, text, summary) => {
|
|
330
|
+
const deliverMessage = (agent, text, summary, reviewPrompt = false) => {
|
|
306
331
|
const message = createUserMessage({
|
|
307
332
|
content: [{
|
|
308
333
|
type: "text",
|
|
@@ -321,6 +346,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
321
346
|
wake.followup(message);
|
|
322
347
|
skipNextCadenceFire.set(agent.session.id, true);
|
|
323
348
|
} else agent.inject(message);
|
|
349
|
+
if (reviewPrompt) markReviewChannel(agent.session.id);
|
|
324
350
|
return true;
|
|
325
351
|
} catch (error) {
|
|
326
352
|
ctx.logger.warn(`dsh-evolution-review: review delivery failed (${error instanceof Error ? error.message : String(error)}) — nothing was queued; the review is NOT consumed and retries at the next completed boundary`);
|
|
@@ -354,7 +380,10 @@ function apply(ctx, rawConfig = {}) {
|
|
|
354
380
|
const hashes = /* @__PURE__ */ new Map();
|
|
355
381
|
const io = ctx.get("evolutionIo");
|
|
356
382
|
if (!io) return hashes;
|
|
357
|
-
const library =
|
|
383
|
+
const library = newSkillLibrary({
|
|
384
|
+
config: rootConfig,
|
|
385
|
+
io: evolutionIoAdapter(() => io.provider())
|
|
386
|
+
});
|
|
358
387
|
for (const summary of await library.list({ withContent: true }).catch((error) => {
|
|
359
388
|
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`);
|
|
360
389
|
return [];
|
|
@@ -388,6 +417,13 @@ function apply(ctx, rawConfig = {}) {
|
|
|
388
417
|
const reviewText = redactSecrets(buildReviewRequest(session, kind, signal, config.reviewContextMessages, config.reviewMessageChars));
|
|
389
418
|
const agentOptions = { model };
|
|
390
419
|
if (config.reviewProvider) agentOptions.provider = config.reviewProvider;
|
|
420
|
+
const registry = ctx.get("tools");
|
|
421
|
+
const requestedReviewTools = config.reviewToolAllow ?? [];
|
|
422
|
+
if (requestedReviewTools.length > 0) if (registry === void 0) ctx.logger.warn("dsh-evolution-review: tools service not mounted — the review subagent tool allow-list was not probed; passing it through unchanged");
|
|
423
|
+
else {
|
|
424
|
+
const notInGlobalLayer = requestedReviewTools.filter((name) => registry.get(name) == null);
|
|
425
|
+
if (notInGlobalLayer.length > 0) ctx.logger.warn(`dsh-evolution-review: reviewToolAllow name(s) ${notInGlobalLayer.map((name) => `"${name}"`).join(", ")} are absent from the GLOBAL tool layer — passing them through unchanged (a preset-mounted tool is invisible to a scope-less lookup)`);
|
|
426
|
+
}
|
|
391
427
|
const preRunHashes = await treeSkillHashes();
|
|
392
428
|
const sessionSeqAtPlanTime = session.seq - 1;
|
|
393
429
|
const run = await subagents.start("spawn", {
|
|
@@ -401,7 +437,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
401
437
|
maxDepth: config.reviewMaxDepth,
|
|
402
438
|
agentOptions,
|
|
403
439
|
persona: reviewPrompt(kind, "plan"),
|
|
404
|
-
toolFilter: { allow:
|
|
440
|
+
toolFilter: { allow: requestedReviewTools },
|
|
405
441
|
outputSchema: REVIEW_OUTPUT_SCHEMA
|
|
406
442
|
});
|
|
407
443
|
try {
|
|
@@ -412,9 +448,8 @@ function apply(ctx, rawConfig = {}) {
|
|
|
412
448
|
} catch (emitError) {
|
|
413
449
|
ctx.logger.warn(`dsh-evolution-review: review-error emit failed: ${emitError instanceof Error ? emitError.message : String(emitError)}`);
|
|
414
450
|
}
|
|
415
|
-
const
|
|
416
|
-
const
|
|
417
|
-
const diagDetail = typeof failureShape.diagnostic === "string" ? failureShape.diagnostic : void 0;
|
|
451
|
+
const stopDetail = typeof result.stopReason === "string" ? result.stopReason : void 0;
|
|
452
|
+
const diagDetail = typeof result.diagnostic === "string" ? result.diagnostic : void 0;
|
|
418
453
|
ctx.logger.warn(`dsh-evolution-review: review subagent returned no structured plan${stopDetail !== void 0 ? ` (stopReason=${stopDetail}${diagDetail !== void 0 ? `; diagnostic=${diagDetail}` : ""})` : ""}`);
|
|
419
454
|
return false;
|
|
420
455
|
}
|
|
@@ -499,7 +534,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
499
534
|
reviewInFlight = false;
|
|
500
535
|
const deferred = deferredFallbackReviews.splice(0);
|
|
501
536
|
for (const { agent: waitingAgent, sessionId: entrySession, kind: waitingKind, prompt, label, channel, counts: entryCounts } of deferred) {
|
|
502
|
-
if (!deliverMessage(waitingAgent, prompt, label)) {
|
|
537
|
+
if (!deliverMessage(waitingAgent, prompt, label, true)) {
|
|
503
538
|
if (channel === "completion") completionInjected.delete(entrySession);
|
|
504
539
|
continue;
|
|
505
540
|
}
|
|
@@ -525,7 +560,10 @@ function apply(ctx, rawConfig = {}) {
|
|
|
525
560
|
const hashLibrary = (() => {
|
|
526
561
|
const io = ctx.get("evolutionIo");
|
|
527
562
|
if (!io) return null;
|
|
528
|
-
return
|
|
563
|
+
return newSkillLibrary({
|
|
564
|
+
config: rootConfig,
|
|
565
|
+
io: evolutionIoAdapter(() => io.provider())
|
|
566
|
+
});
|
|
529
567
|
})();
|
|
530
568
|
const origins = resolveOrigins(void 0, true);
|
|
531
569
|
const actions = [];
|
|
@@ -563,19 +601,17 @@ function apply(ctx, rawConfig = {}) {
|
|
|
563
601
|
...op,
|
|
564
602
|
evidence: op.evidence
|
|
565
603
|
};
|
|
566
|
-
let
|
|
567
|
-
if ((args.action === "update" || args.action === "edit") && hashLibrary && typeof args.name === "string" && args.name !== "")
|
|
568
|
-
stageCurrent = await hashLibrary.read(args.name).catch(() => null);
|
|
569
|
-
if (stageCurrent !== null) args.staged_from_sha256 = contentHash(stageCurrent);
|
|
570
|
-
}
|
|
604
|
+
let stagedBytes;
|
|
605
|
+
if ((args.action === "update" || args.action === "edit") && hashLibrary && typeof args.name === "string" && args.name !== "") stagedBytes = await hashLibrary.read(args.name).catch(() => void 0);
|
|
571
606
|
if ((args.action === "write_file" || args.action === "remove_file") && hashLibrary && typeof args.name === "string" && args.name !== "" && typeof args.file_path === "string" && args.file_path !== "") {
|
|
572
607
|
const stageFile = await hashLibrary.readSupportFile(args.name, args.file_path).catch(() => void 0);
|
|
573
608
|
if (stageFile !== void 0) args.staged_from_sha256 = stageFile === null ? "absent" : contentHash(stageFile);
|
|
574
609
|
}
|
|
575
610
|
const opName = typeof args.name === "string" ? args.name : "";
|
|
576
611
|
const hashChecked = (args.action === "update" || args.action === "edit" || args.action === "patch") && opName !== "" && preRunHashes?.has(opName) === true;
|
|
612
|
+
const anchored = hashChecked && typeof stagedBytes === "string" ? stagedBytes : null;
|
|
577
613
|
if (hashChecked) {
|
|
578
|
-
const live =
|
|
614
|
+
const live = anchored !== null ? anchored : hashLibrary ? await hashLibrary.read(opName).catch(() => null) : null;
|
|
579
615
|
const preRun = preRunHashes.get(opName);
|
|
580
616
|
if (live === null || preRun !== void 0 && contentHash(live) !== preRun) {
|
|
581
617
|
ok = false;
|
|
@@ -583,6 +619,8 @@ function apply(ctx, rawConfig = {}) {
|
|
|
583
619
|
continue;
|
|
584
620
|
}
|
|
585
621
|
}
|
|
622
|
+
const anchorBytes = anchored ?? stagedBytes;
|
|
623
|
+
if ((args.action === "update" || args.action === "edit") && anchorBytes !== void 0) args.staged_from_sha256 = anchorBytes === null ? "absent" : contentHash(anchorBytes);
|
|
586
624
|
const runnerArgs = {
|
|
587
625
|
operation: args,
|
|
588
626
|
origin: origins.library
|
|
@@ -670,11 +708,14 @@ function apply(ctx, rawConfig = {}) {
|
|
|
670
708
|
message: "evolution-io service not mounted"
|
|
671
709
|
};
|
|
672
710
|
const policySnapshot = policySnapshotOf(ctx.get("evolutionPolicy"));
|
|
673
|
-
const library =
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
711
|
+
const library = newSkillLibrary({
|
|
712
|
+
config: rootConfig,
|
|
713
|
+
io: evolutionIoAdapter(() => io.provider()),
|
|
714
|
+
limits: {
|
|
715
|
+
...DEFAULT_SKILL_LIMITS,
|
|
716
|
+
maxSkillContentChars: policySnapshot?.skillContentChars ?? DEFAULT_SKILL_LIMITS.maxSkillContentChars
|
|
717
|
+
},
|
|
718
|
+
ctx
|
|
678
719
|
});
|
|
679
720
|
const op = skillArgs;
|
|
680
721
|
const name = op.name ?? "";
|
|
@@ -792,32 +833,27 @@ function staleRefusal(result, name, filePath) {
|
|
|
792
833
|
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.`
|
|
793
834
|
};
|
|
794
835
|
}
|
|
836
|
+
/**
|
|
837
|
+
* v37 P7a: the read-before-write credit now comes from evolution-core's
|
|
838
|
+
* `tool-dispatch` module — the ONE reader of the platform's dispatch event
|
|
839
|
+
* types, and the ONE authority on which tool reads a skill.
|
|
840
|
+
*
|
|
841
|
+
* v32 REV-06(a) is preserved by the normalizer: a skill counts as READ only
|
|
842
|
+
* when it did not fail, so a failed/timeout read still cannot pass the
|
|
843
|
+
* read-before-write gate and let the review blind-overwrite content the model
|
|
844
|
+
* never saw. What changed is the vocabulary the gate listens to: matching
|
|
845
|
+
* `tool/call` here meant every PTC session (`tool/ptc-dispatch*`) collected an
|
|
846
|
+
* EMPTY set, so `filterUnreadSkillOps` dropped every mutating op the model had
|
|
847
|
+
* legitimately read first — and nothing reported the loss.
|
|
848
|
+
* @param session - the session whose log is folded.
|
|
849
|
+
* @returns the skill names this session read through a non-failed dispatch.
|
|
850
|
+
*/
|
|
795
851
|
function collectReadSkillNames(session) {
|
|
796
|
-
const callNames = /* @__PURE__ */ new Map();
|
|
797
|
-
const okCallIds = /* @__PURE__ */ new Set();
|
|
798
|
-
for (const event of session.snapshotEvents()) if (event.type === "tool/call") {
|
|
799
|
-
if (event.data.name !== "skill") continue;
|
|
800
|
-
const raw = event.data.arguments;
|
|
801
|
-
let parsed = {};
|
|
802
|
-
if (typeof raw === "string") try {
|
|
803
|
-
parsed = JSON.parse(raw);
|
|
804
|
-
} catch {
|
|
805
|
-
continue;
|
|
806
|
-
}
|
|
807
|
-
else parsed = raw;
|
|
808
|
-
const parsedObj = typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
809
|
-
const name = typeof parsedObj.name === "string" ? parsedObj.name : typeof parsedObj.skill === "string" ? parsedObj.skill : "";
|
|
810
|
-
if (name) callNames.set(event.data.callId, name);
|
|
811
|
-
} else if (event.type === "tool/result") {
|
|
812
|
-
const blocks = event.data?.message?.content;
|
|
813
|
-
if (!Array.isArray(blocks)) continue;
|
|
814
|
-
for (const block of blocks) {
|
|
815
|
-
const typed = block;
|
|
816
|
-
if (typed.type === "tool-result" && typed.isError !== true && typeof typed.toolCallId === "string") okCallIds.add(typed.toolCallId);
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
852
|
const names = /* @__PURE__ */ new Set();
|
|
820
|
-
for (const
|
|
853
|
+
for (const dispatch of foldToolDispatches(session.snapshotEvents())) {
|
|
854
|
+
const name = skillReadNameOf(dispatch);
|
|
855
|
+
if (name !== void 0) names.add(name);
|
|
856
|
+
}
|
|
821
857
|
return names;
|
|
822
858
|
}
|
|
823
859
|
/** Map/set size that triggers a dead-session counter sweep (bounded, not a hard cap). */
|
|
@@ -874,7 +910,26 @@ function fingerprintPolicy(snapshot) {
|
|
|
874
910
|
}
|
|
875
911
|
}
|
|
876
912
|
/**
|
|
877
|
-
*
|
|
913
|
+
* The call one log event ANSWERS, in either platform vocabulary.
|
|
914
|
+
*
|
|
915
|
+
* A native tool-result event names its call inside the message (`message.source`,
|
|
916
|
+
* the shape `createToolResultMessage` produces — llm/src/message.ts:258), and a
|
|
917
|
+
* PTC settle event carries `isError` plus the outcome `content` on its own
|
|
918
|
+
* payload. Classified structurally, from the payload, so this file never
|
|
919
|
+
* discriminates on a dispatch event type (arch guard N11).
|
|
920
|
+
* @param event - one persisted session event.
|
|
921
|
+
* @returns the answered call id, or `null` when the event carries no outcome.
|
|
922
|
+
*/
|
|
923
|
+
function resultCallIdOf(event) {
|
|
924
|
+
const data = event?.data;
|
|
925
|
+
if (data === void 0 || data === null) return null;
|
|
926
|
+
const native = data.message?.source?.callId;
|
|
927
|
+
if (typeof native === "string") return native;
|
|
928
|
+
if (typeof data.subCallId === "string" && typeof data.isError === "boolean") return data.subCallId;
|
|
929
|
+
return null;
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* V10-10 (P2-11): render one `[result]` evidence line from a tool-result
|
|
878
933
|
* event payload. The former read (`data.output`) targeted a field that does
|
|
879
934
|
* not exist on the upstream rc.2 payload, so EVERY result line rendered an
|
|
880
935
|
* empty payload and the review subagent never saw tool output — the evidence
|
|
@@ -902,13 +957,21 @@ function buildReviewRequest(session, kind, signal, maxMessages, maxMessageChars)
|
|
|
902
957
|
}
|
|
903
958
|
const toolLines = [];
|
|
904
959
|
const events = session.snapshotEvents();
|
|
960
|
+
const openedCallIds = /* @__PURE__ */ new Set();
|
|
905
961
|
for (let index = events.length - 1; index >= 0 && toolLines.length < 12; index -= 1) {
|
|
906
962
|
const event = events[index];
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
963
|
+
const answeredCallId = resultCallIdOf(event);
|
|
964
|
+
if (answeredCallId !== null) {
|
|
965
|
+
if (openedCallIds.has(answeredCallId)) continue;
|
|
966
|
+
openedCallIds.add(answeredCallId);
|
|
967
|
+
toolLines.push(renderToolResultLine(event?.data));
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
970
|
+
const opened = readDispatchSignal(event);
|
|
971
|
+
if (opened === null || openedCallIds.has(opened.callId)) continue;
|
|
972
|
+
openedCallIds.add(opened.callId);
|
|
973
|
+
const argsRaw = typeof opened.arguments === "string" ? opened.arguments : JSON.stringify(opened.arguments ?? {});
|
|
974
|
+
toolLines.push(`[call] ${opened.name} ${argsRaw.slice(0, 500)}`);
|
|
912
975
|
}
|
|
913
976
|
toolLines.reverse();
|
|
914
977
|
return [
|
package/lib/types/index.d.ts
CHANGED
|
@@ -17,9 +17,12 @@ export interface Config {
|
|
|
17
17
|
* system prompt plus a re-serialized conversation digest
|
|
18
18
|
* (`buildReviewRequest`, redacted, header-prepended) on a DIFFERENT model —
|
|
19
19
|
* so no prefix cache is shared with the parent and the input tokens are paid
|
|
20
|
-
* at full price. Deployments that prefer the clean-context split
|
|
21
|
-
*
|
|
20
|
+
* at full price. Deployments that prefer the clean-context split switch the
|
|
21
|
+
* POLICY row (`evolution-policy`): the mounted policy snapshot shadows this
|
|
22
|
+
* row's value, which the plugin reports once at load (v37 P2-24). */
|
|
22
23
|
reviewMode?: 'subagent' | 'inject';
|
|
24
|
+
/** Shadowed by the policy snapshot in every shipped composition — configure
|
|
25
|
+
* `reviewMemoryInterval` on the `evolution-policy` row instead (v37 P2-24). */
|
|
23
26
|
memoryInterval?: number;
|
|
24
27
|
skillInterval?: number;
|
|
25
28
|
/**
|
|
@@ -138,7 +141,7 @@ export declare function filterUnreadSkillOps(ops: Array<{
|
|
|
138
141
|
name?: string;
|
|
139
142
|
}>, readNames: ReadonlySet<string>): number;
|
|
140
143
|
/**
|
|
141
|
-
* V10-10 (P2-11): render one `[result]` evidence line from a
|
|
144
|
+
* V10-10 (P2-11): render one `[result]` evidence line from a tool-result
|
|
142
145
|
* event payload. The former read (`data.output`) targeted a field that does
|
|
143
146
|
* not exist on the upstream rc.2 payload, so EVERY result line rendered an
|
|
144
147
|
* empty payload and the review subagent never saw tool output — the evidence
|
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.76",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -18,10 +18,6 @@
|
|
|
18
18
|
"types": "./lib/types/index.d.ts",
|
|
19
19
|
"default": "./lib/index.js"
|
|
20
20
|
},
|
|
21
|
-
"./invariant": {
|
|
22
|
-
"types": "./lib/types/invariant.d.ts",
|
|
23
|
-
"default": "./lib/invariant.js"
|
|
24
|
-
},
|
|
25
21
|
"./package.json": "./package.json"
|
|
26
22
|
},
|
|
27
23
|
"files": [
|
|
@@ -31,33 +27,31 @@
|
|
|
31
27
|
"license": "MIT",
|
|
32
28
|
"dependencies": {
|
|
33
29
|
"@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.
|
|
30
|
+
"@lmzhen/dsh-evolution-approval": "^0.3.76",
|
|
31
|
+
"@lmzhen/dsh-evolution-core": "^0.3.76",
|
|
32
|
+
"@lmzhen/dsh-evolution-plan-validator": "^0.3.76"
|
|
37
33
|
},
|
|
38
34
|
"peerDependencies": {
|
|
39
35
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
40
36
|
"@deepseek-ai/dsh-agent": "^0.1.5-rc.2",
|
|
41
|
-
"@deepseek-ai/dsh-invariants": "^0.1.5-rc.2",
|
|
42
37
|
"@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
|
|
43
38
|
"@deepseek-ai/dsh-session": "^0.1.5-rc.2",
|
|
44
39
|
"@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
|
|
45
|
-
"@lmzhen/dsh-evolution-state": "^0.3.
|
|
46
|
-
"@lmzhen/dsh-evolution-policy": "^0.3.
|
|
40
|
+
"@lmzhen/dsh-evolution-state": "^0.3.76",
|
|
41
|
+
"@lmzhen/dsh-evolution-policy": "^0.3.76"
|
|
47
42
|
},
|
|
48
43
|
"devDependencies": {
|
|
49
44
|
"@deepseek-ai/dsh-agent": "^0.1.5-rc.2",
|
|
50
45
|
"@deepseek-ai/dsh-agent-loop-testkit": "^0.1.5-rc.2",
|
|
51
|
-
"@deepseek-ai/dsh-invariants": "^0.1.5-rc.2",
|
|
52
46
|
"@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
|
|
53
47
|
"@deepseek-ai/dsh-session": "^0.1.5-rc.2",
|
|
54
48
|
"@deepseek-ai/dsh-session-persistence": "^0.1.5-rc.2",
|
|
55
49
|
"@deepseek-ai/dsh-session-persistence-jsonl": "^0.1.5-rc.2",
|
|
56
50
|
"@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
|
|
57
|
-
"@lmzhen/dsh-evolution-approval": "^0.3.
|
|
58
|
-
"@lmzhen/dsh-evolution-core": "^0.3.
|
|
59
|
-
"@lmzhen/dsh-evolution-curator": "^0.3.
|
|
60
|
-
"@lmzhen/dsh-evolution-plan-validator": "^0.3.
|
|
61
|
-
"@lmzhen/dsh-evolution-state": "^0.3.
|
|
51
|
+
"@lmzhen/dsh-evolution-approval": "^0.3.76",
|
|
52
|
+
"@lmzhen/dsh-evolution-core": "^0.3.76",
|
|
53
|
+
"@lmzhen/dsh-evolution-curator": "^0.3.76",
|
|
54
|
+
"@lmzhen/dsh-evolution-plan-validator": "^0.3.76",
|
|
55
|
+
"@lmzhen/dsh-evolution-state": "^0.3.76"
|
|
62
56
|
}
|
|
63
57
|
}
|
package/lib/invariant.js
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
//#region lib/types/invariant.js
|
|
2
|
-
const PACKAGE_NAME = "@lmzhen/dsh-evolution-review";
|
|
3
|
-
const name = "evolution-review-invariant";
|
|
4
|
-
const inject = ["invariants"];
|
|
5
|
-
const install = () => {};
|
|
6
|
-
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
7
|
-
//#endregion
|
|
8
|
-
export { apply, inject, name };
|
package/lib/types/invariant.d.ts
DELETED