@lmzhen/dsh-evolution-review 0.3.74 → 0.3.77
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 +118 -53
- package/lib/types/index.d.ts +14 -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, sessionAudited, 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
|
/**
|
|
@@ -27,6 +28,7 @@ const Config = z.object({
|
|
|
27
28
|
z.const("both")
|
|
28
29
|
]).default(DEFAULT_SKILL_REVIEW_TRIGGER),
|
|
29
30
|
reviewWakeInject: z.boolean().default(true),
|
|
31
|
+
sessionScoped: z.boolean().default(false),
|
|
30
32
|
skillReviewCompletionMinToolCalls: z.number().min(1).default(DEFAULT_SKILL_REVIEW_COMPLETION_MIN_TOOL_CALLS),
|
|
31
33
|
root: z.string().default(""),
|
|
32
34
|
skillsRoot: z.string().default("")
|
|
@@ -92,6 +94,25 @@ function apply(ctx, rawConfig = {}) {
|
|
|
92
94
|
const cadenceResetWarned = /* @__PURE__ */ new Set();
|
|
93
95
|
let reviewInFlight = false;
|
|
94
96
|
const policy = () => ctx.get("evolutionPolicy")?.get();
|
|
97
|
+
const schemaDefaults = Config["~standard"].validate({}).value;
|
|
98
|
+
const shadowedRowFields = [
|
|
99
|
+
"reviewMode",
|
|
100
|
+
"memoryInterval",
|
|
101
|
+
"skillInterval"
|
|
102
|
+
].filter((field) => rawConfig[field] !== void 0 && rawConfig[field] !== schemaDefaults[field]);
|
|
103
|
+
if (shadowedRowFields.length > 0) {
|
|
104
|
+
let shadowWarned = false;
|
|
105
|
+
const warnShadowed = () => {
|
|
106
|
+
if (shadowWarned) return;
|
|
107
|
+
shadowWarned = true;
|
|
108
|
+
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.`);
|
|
109
|
+
};
|
|
110
|
+
if (policy() !== void 0) warnShadowed();
|
|
111
|
+
else ctx.inject(["evolutionPolicy"], () => {
|
|
112
|
+
warnShadowed();
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
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
116
|
const reviewStateLocks = /* @__PURE__ */ new Map();
|
|
96
117
|
async function withReviewStateLock(id, task) {
|
|
97
118
|
const next = (reviewStateLocks.get(id) ?? Promise.resolve()).catch(() => {}).then(task);
|
|
@@ -103,7 +124,12 @@ function apply(ctx, rawConfig = {}) {
|
|
|
103
124
|
}
|
|
104
125
|
}
|
|
105
126
|
ctx.on("session/event", (session, event) => {
|
|
127
|
+
if (!sessionAudited(ctx, session.id, config.sessionScoped)) return;
|
|
106
128
|
if (event.type === "turn/start" && session.header.origin !== "subagent") turnStarts.set(session.id, session.seq - 1);
|
|
129
|
+
if (event.type === "user/message") {
|
|
130
|
+
if ((event.data?.source)?.kind === "user") clearReviewChannel(session.id);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
107
133
|
if (event.type !== "turn/end") return;
|
|
108
134
|
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
135
|
const isAlive = (id) => ctx.agents.get(id) !== void 0;
|
|
@@ -114,6 +140,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
114
140
|
sweepDeadSessionEntries(pendingCadenceWarned, isAlive);
|
|
115
141
|
sweepDeadSessionEntries(skipNextCadenceFire, isAlive);
|
|
116
142
|
sweepDeadSessionEntries(cadenceResetWarned, isAlive);
|
|
143
|
+
sweepReviewChannelSessions((id) => ctx.agents.get(SessionId(id)) !== void 0);
|
|
117
144
|
}
|
|
118
145
|
onTurnEnd(session, event);
|
|
119
146
|
});
|
|
@@ -174,7 +201,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
174
201
|
if (pendingKind !== void 0) {
|
|
175
202
|
pendingCadenceReviews.delete(session.id);
|
|
176
203
|
if ((policy()?.reviewMode ?? config.reviewMode) === "inject") {
|
|
177
|
-
if (!deliverMessage(agent, reviewPrompt(pendingKind), "auto-review")) {
|
|
204
|
+
if (!deliverMessage(agent, reviewPrompt(pendingKind), "auto-review", true)) {
|
|
178
205
|
pendingCadenceReviews.set(session.id, pendingKind);
|
|
179
206
|
return;
|
|
180
207
|
}
|
|
@@ -208,7 +235,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
208
235
|
pendingCadenceReviews.set(session.id, pendingKind);
|
|
209
236
|
return;
|
|
210
237
|
} else if (reviewOutcome !== "deferred") {
|
|
211
|
-
if (!deliverMessage(agent, reviewPrompt(pendingKind), "auto-review")) {
|
|
238
|
+
if (!deliverMessage(agent, reviewPrompt(pendingKind), "auto-review", true)) {
|
|
212
239
|
pendingCadenceReviews.set(session.id, pendingKind);
|
|
213
240
|
return;
|
|
214
241
|
}
|
|
@@ -272,7 +299,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
272
299
|
}
|
|
273
300
|
return;
|
|
274
301
|
}
|
|
275
|
-
if (!deliverMessage(agent, COMPLETION_SKILL_REVIEW_PROMPT, "completion review")) {
|
|
302
|
+
if (!deliverMessage(agent, COMPLETION_SKILL_REVIEW_PROMPT, "completion review", true)) {
|
|
276
303
|
completionInjected.delete(session.id);
|
|
277
304
|
return;
|
|
278
305
|
}
|
|
@@ -302,7 +329,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
302
329
|
* prototype method), the caller's catch demoted that to a console warning,
|
|
303
330
|
* and the cadence reset ran anyway — the segment's review was consumed with
|
|
304
331
|
* nothing queued (silent no-delivery window: 2026-09-07 → 0.3.73). */
|
|
305
|
-
const deliverMessage = (agent, text, summary) => {
|
|
332
|
+
const deliverMessage = (agent, text, summary, reviewPrompt = false) => {
|
|
306
333
|
const message = createUserMessage({
|
|
307
334
|
content: [{
|
|
308
335
|
type: "text",
|
|
@@ -321,6 +348,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
321
348
|
wake.followup(message);
|
|
322
349
|
skipNextCadenceFire.set(agent.session.id, true);
|
|
323
350
|
} else agent.inject(message);
|
|
351
|
+
if (reviewPrompt) markReviewChannel(agent.session.id);
|
|
324
352
|
return true;
|
|
325
353
|
} catch (error) {
|
|
326
354
|
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 +382,10 @@ function apply(ctx, rawConfig = {}) {
|
|
|
354
382
|
const hashes = /* @__PURE__ */ new Map();
|
|
355
383
|
const io = ctx.get("evolutionIo");
|
|
356
384
|
if (!io) return hashes;
|
|
357
|
-
const library =
|
|
385
|
+
const library = newSkillLibrary({
|
|
386
|
+
config: rootConfig,
|
|
387
|
+
io: evolutionIoAdapter(() => io.provider())
|
|
388
|
+
});
|
|
358
389
|
for (const summary of await library.list({ withContent: true }).catch((error) => {
|
|
359
390
|
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
391
|
return [];
|
|
@@ -388,6 +419,13 @@ function apply(ctx, rawConfig = {}) {
|
|
|
388
419
|
const reviewText = redactSecrets(buildReviewRequest(session, kind, signal, config.reviewContextMessages, config.reviewMessageChars));
|
|
389
420
|
const agentOptions = { model };
|
|
390
421
|
if (config.reviewProvider) agentOptions.provider = config.reviewProvider;
|
|
422
|
+
const registry = ctx.get("tools");
|
|
423
|
+
const requestedReviewTools = config.reviewToolAllow ?? [];
|
|
424
|
+
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");
|
|
425
|
+
else {
|
|
426
|
+
const notInGlobalLayer = requestedReviewTools.filter((name) => registry.get(name) == null);
|
|
427
|
+
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)`);
|
|
428
|
+
}
|
|
391
429
|
const preRunHashes = await treeSkillHashes();
|
|
392
430
|
const sessionSeqAtPlanTime = session.seq - 1;
|
|
393
431
|
const run = await subagents.start("spawn", {
|
|
@@ -401,7 +439,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
401
439
|
maxDepth: config.reviewMaxDepth,
|
|
402
440
|
agentOptions,
|
|
403
441
|
persona: reviewPrompt(kind, "plan"),
|
|
404
|
-
toolFilter: { allow:
|
|
442
|
+
toolFilter: { allow: requestedReviewTools },
|
|
405
443
|
outputSchema: REVIEW_OUTPUT_SCHEMA
|
|
406
444
|
});
|
|
407
445
|
try {
|
|
@@ -412,9 +450,8 @@ function apply(ctx, rawConfig = {}) {
|
|
|
412
450
|
} catch (emitError) {
|
|
413
451
|
ctx.logger.warn(`dsh-evolution-review: review-error emit failed: ${emitError instanceof Error ? emitError.message : String(emitError)}`);
|
|
414
452
|
}
|
|
415
|
-
const
|
|
416
|
-
const
|
|
417
|
-
const diagDetail = typeof failureShape.diagnostic === "string" ? failureShape.diagnostic : void 0;
|
|
453
|
+
const stopDetail = typeof result.stopReason === "string" ? result.stopReason : void 0;
|
|
454
|
+
const diagDetail = typeof result.diagnostic === "string" ? result.diagnostic : void 0;
|
|
418
455
|
ctx.logger.warn(`dsh-evolution-review: review subagent returned no structured plan${stopDetail !== void 0 ? ` (stopReason=${stopDetail}${diagDetail !== void 0 ? `; diagnostic=${diagDetail}` : ""})` : ""}`);
|
|
419
456
|
return false;
|
|
420
457
|
}
|
|
@@ -499,7 +536,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
499
536
|
reviewInFlight = false;
|
|
500
537
|
const deferred = deferredFallbackReviews.splice(0);
|
|
501
538
|
for (const { agent: waitingAgent, sessionId: entrySession, kind: waitingKind, prompt, label, channel, counts: entryCounts } of deferred) {
|
|
502
|
-
if (!deliverMessage(waitingAgent, prompt, label)) {
|
|
539
|
+
if (!deliverMessage(waitingAgent, prompt, label, true)) {
|
|
503
540
|
if (channel === "completion") completionInjected.delete(entrySession);
|
|
504
541
|
continue;
|
|
505
542
|
}
|
|
@@ -525,7 +562,10 @@ function apply(ctx, rawConfig = {}) {
|
|
|
525
562
|
const hashLibrary = (() => {
|
|
526
563
|
const io = ctx.get("evolutionIo");
|
|
527
564
|
if (!io) return null;
|
|
528
|
-
return
|
|
565
|
+
return newSkillLibrary({
|
|
566
|
+
config: rootConfig,
|
|
567
|
+
io: evolutionIoAdapter(() => io.provider())
|
|
568
|
+
});
|
|
529
569
|
})();
|
|
530
570
|
const origins = resolveOrigins(void 0, true);
|
|
531
571
|
const actions = [];
|
|
@@ -563,19 +603,17 @@ function apply(ctx, rawConfig = {}) {
|
|
|
563
603
|
...op,
|
|
564
604
|
evidence: op.evidence
|
|
565
605
|
};
|
|
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
|
-
}
|
|
606
|
+
let stagedBytes;
|
|
607
|
+
if ((args.action === "update" || args.action === "edit") && hashLibrary && typeof args.name === "string" && args.name !== "") stagedBytes = await hashLibrary.read(args.name).catch(() => void 0);
|
|
571
608
|
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
609
|
const stageFile = await hashLibrary.readSupportFile(args.name, args.file_path).catch(() => void 0);
|
|
573
610
|
if (stageFile !== void 0) args.staged_from_sha256 = stageFile === null ? "absent" : contentHash(stageFile);
|
|
574
611
|
}
|
|
575
612
|
const opName = typeof args.name === "string" ? args.name : "";
|
|
576
613
|
const hashChecked = (args.action === "update" || args.action === "edit" || args.action === "patch") && opName !== "" && preRunHashes?.has(opName) === true;
|
|
614
|
+
const anchored = hashChecked && typeof stagedBytes === "string" ? stagedBytes : null;
|
|
577
615
|
if (hashChecked) {
|
|
578
|
-
const live =
|
|
616
|
+
const live = anchored !== null ? anchored : hashLibrary ? await hashLibrary.read(opName).catch(() => null) : null;
|
|
579
617
|
const preRun = preRunHashes.get(opName);
|
|
580
618
|
if (live === null || preRun !== void 0 && contentHash(live) !== preRun) {
|
|
581
619
|
ok = false;
|
|
@@ -583,6 +621,8 @@ function apply(ctx, rawConfig = {}) {
|
|
|
583
621
|
continue;
|
|
584
622
|
}
|
|
585
623
|
}
|
|
624
|
+
const anchorBytes = anchored ?? stagedBytes;
|
|
625
|
+
if ((args.action === "update" || args.action === "edit") && anchorBytes !== void 0) args.staged_from_sha256 = anchorBytes === null ? "absent" : contentHash(anchorBytes);
|
|
586
626
|
const runnerArgs = {
|
|
587
627
|
operation: args,
|
|
588
628
|
origin: origins.library
|
|
@@ -670,11 +710,14 @@ function apply(ctx, rawConfig = {}) {
|
|
|
670
710
|
message: "evolution-io service not mounted"
|
|
671
711
|
};
|
|
672
712
|
const policySnapshot = policySnapshotOf(ctx.get("evolutionPolicy"));
|
|
673
|
-
const library =
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
713
|
+
const library = newSkillLibrary({
|
|
714
|
+
config: rootConfig,
|
|
715
|
+
io: evolutionIoAdapter(() => io.provider()),
|
|
716
|
+
limits: {
|
|
717
|
+
...DEFAULT_SKILL_LIMITS,
|
|
718
|
+
maxSkillContentChars: policySnapshot?.skillContentChars ?? DEFAULT_SKILL_LIMITS.maxSkillContentChars
|
|
719
|
+
},
|
|
720
|
+
ctx
|
|
678
721
|
});
|
|
679
722
|
const op = skillArgs;
|
|
680
723
|
const name = op.name ?? "";
|
|
@@ -792,32 +835,27 @@ function staleRefusal(result, name, filePath) {
|
|
|
792
835
|
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
836
|
};
|
|
794
837
|
}
|
|
838
|
+
/**
|
|
839
|
+
* v37 P7a: the read-before-write credit now comes from evolution-core's
|
|
840
|
+
* `tool-dispatch` module — the ONE reader of the platform's dispatch event
|
|
841
|
+
* types, and the ONE authority on which tool reads a skill.
|
|
842
|
+
*
|
|
843
|
+
* v32 REV-06(a) is preserved by the normalizer: a skill counts as READ only
|
|
844
|
+
* when it did not fail, so a failed/timeout read still cannot pass the
|
|
845
|
+
* read-before-write gate and let the review blind-overwrite content the model
|
|
846
|
+
* never saw. What changed is the vocabulary the gate listens to: matching
|
|
847
|
+
* `tool/call` here meant every PTC session (`tool/ptc-dispatch*`) collected an
|
|
848
|
+
* EMPTY set, so `filterUnreadSkillOps` dropped every mutating op the model had
|
|
849
|
+
* legitimately read first — and nothing reported the loss.
|
|
850
|
+
* @param session - the session whose log is folded.
|
|
851
|
+
* @returns the skill names this session read through a non-failed dispatch.
|
|
852
|
+
*/
|
|
795
853
|
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
854
|
const names = /* @__PURE__ */ new Set();
|
|
820
|
-
for (const
|
|
855
|
+
for (const dispatch of foldToolDispatches(session.snapshotEvents())) {
|
|
856
|
+
const name = skillReadNameOf(dispatch);
|
|
857
|
+
if (name !== void 0) names.add(name);
|
|
858
|
+
}
|
|
821
859
|
return names;
|
|
822
860
|
}
|
|
823
861
|
/** Map/set size that triggers a dead-session counter sweep (bounded, not a hard cap). */
|
|
@@ -874,7 +912,26 @@ function fingerprintPolicy(snapshot) {
|
|
|
874
912
|
}
|
|
875
913
|
}
|
|
876
914
|
/**
|
|
877
|
-
*
|
|
915
|
+
* The call one log event ANSWERS, in either platform vocabulary.
|
|
916
|
+
*
|
|
917
|
+
* A native tool-result event names its call inside the message (`message.source`,
|
|
918
|
+
* the shape `createToolResultMessage` produces — llm/src/message.ts:258), and a
|
|
919
|
+
* PTC settle event carries `isError` plus the outcome `content` on its own
|
|
920
|
+
* payload. Classified structurally, from the payload, so this file never
|
|
921
|
+
* discriminates on a dispatch event type (arch guard N11).
|
|
922
|
+
* @param event - one persisted session event.
|
|
923
|
+
* @returns the answered call id, or `null` when the event carries no outcome.
|
|
924
|
+
*/
|
|
925
|
+
function resultCallIdOf(event) {
|
|
926
|
+
const data = event?.data;
|
|
927
|
+
if (data === void 0 || data === null) return null;
|
|
928
|
+
const native = data.message?.source?.callId;
|
|
929
|
+
if (typeof native === "string") return native;
|
|
930
|
+
if (typeof data.subCallId === "string" && typeof data.isError === "boolean") return data.subCallId;
|
|
931
|
+
return null;
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* V10-10 (P2-11): render one `[result]` evidence line from a tool-result
|
|
878
935
|
* event payload. The former read (`data.output`) targeted a field that does
|
|
879
936
|
* not exist on the upstream rc.2 payload, so EVERY result line rendered an
|
|
880
937
|
* empty payload and the review subagent never saw tool output — the evidence
|
|
@@ -902,13 +959,21 @@ function buildReviewRequest(session, kind, signal, maxMessages, maxMessageChars)
|
|
|
902
959
|
}
|
|
903
960
|
const toolLines = [];
|
|
904
961
|
const events = session.snapshotEvents();
|
|
962
|
+
const openedCallIds = /* @__PURE__ */ new Set();
|
|
905
963
|
for (let index = events.length - 1; index >= 0 && toolLines.length < 12; index -= 1) {
|
|
906
964
|
const event = events[index];
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
965
|
+
const answeredCallId = resultCallIdOf(event);
|
|
966
|
+
if (answeredCallId !== null) {
|
|
967
|
+
if (openedCallIds.has(answeredCallId)) continue;
|
|
968
|
+
openedCallIds.add(answeredCallId);
|
|
969
|
+
toolLines.push(renderToolResultLine(event?.data));
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
const opened = readDispatchSignal(event);
|
|
973
|
+
if (opened === null || openedCallIds.has(opened.callId)) continue;
|
|
974
|
+
openedCallIds.add(opened.callId);
|
|
975
|
+
const argsRaw = typeof opened.arguments === "string" ? opened.arguments : JSON.stringify(opened.arguments ?? {});
|
|
976
|
+
toolLines.push(`[call] ${opened.name} ${argsRaw.slice(0, 500)}`);
|
|
912
977
|
}
|
|
913
978
|
toolLines.reverse();
|
|
914
979
|
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
|
/**
|
|
@@ -62,6 +65,14 @@ export interface Config {
|
|
|
62
65
|
* config that still sets it reaches {@link assertSkillsRootAliasRetired} and
|
|
63
66
|
* fails the load instead of being silently dropped. Never read. */
|
|
64
67
|
skillsRoot?: string;
|
|
68
|
+
/**
|
|
69
|
+
* Act only on sessions that carry the family's model tools (evolution-core's
|
|
70
|
+
* per-session probe). The shipped bundles set it: at profile root every
|
|
71
|
+
* session carries them, inside a variant preset only the sessions that
|
|
72
|
+
* selected one do. False — a bare library mount, the host-only infrastructure
|
|
73
|
+
* mode, and every deployment that predates this field — acts on every session.
|
|
74
|
+
*/
|
|
75
|
+
sessionScoped?: boolean;
|
|
65
76
|
}
|
|
66
77
|
export declare const Config: z<Config>;
|
|
67
78
|
/** V8-01 (0.3.45): the review subagent's structured-output contract. The
|
|
@@ -138,7 +149,7 @@ export declare function filterUnreadSkillOps(ops: Array<{
|
|
|
138
149
|
name?: string;
|
|
139
150
|
}>, readNames: ReadonlySet<string>): number;
|
|
140
151
|
/**
|
|
141
|
-
* V10-10 (P2-11): render one `[result]` evidence line from a
|
|
152
|
+
* V10-10 (P2-11): render one `[result]` evidence line from a tool-result
|
|
142
153
|
* event payload. The former read (`data.output`) targeted a field that does
|
|
143
154
|
* not exist on the upstream rc.2 payload, so EVERY result line rendered an
|
|
144
155
|
* 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.77",
|
|
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.77",
|
|
31
|
+
"@lmzhen/dsh-evolution-core": "^0.3.77",
|
|
32
|
+
"@lmzhen/dsh-evolution-plan-validator": "^0.3.77"
|
|
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.77",
|
|
41
|
+
"@lmzhen/dsh-evolution-policy": "^0.3.77"
|
|
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.77",
|
|
52
|
+
"@lmzhen/dsh-evolution-core": "^0.3.77",
|
|
53
|
+
"@lmzhen/dsh-evolution-curator": "^0.3.77",
|
|
54
|
+
"@lmzhen/dsh-evolution-plan-validator": "^0.3.77",
|
|
55
|
+
"@lmzhen/dsh-evolution-state": "^0.3.77"
|
|
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