@lmzhen/dsh-evolution-review 0.15.1 → 0.16.0
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 -3
- package/lib/index.js +194 -96
- package/lib/types/review-notice.d.ts +74 -0
- package/lib/types/session-state.d.ts +3 -0
- package/package.json +11 -11
package/README.md
CHANGED
|
@@ -22,6 +22,8 @@ own, and the delivery mode lives on the `evolution-policy` row, not here.
|
|
|
22
22
|
- `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`.
|
|
23
23
|
- 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.
|
|
24
24
|
- 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.
|
|
25
|
+
- **Hosts without `agent.inbox` fall back to delivery-time settlement.** With no queue to observe there is no `claimed`/`discarded` to wait for, so the delivery itself closes the window (`delivery-no-inbox`) and each completed boundary may deliver again: that is the pre-0.3.41 behavior, and under `interval=1` it can chain review turns. Every current host exposes `Agent.inbox`, so this is the degradation path for stub or older agents (the plugin's own tests rely on it).
|
|
26
|
+
- **The subagent channel settles on completion, not on a queue event.** A subagent review never enters the parent's queue, so when the run finishes the window restarts (`subagent-flush`) — including any notice an earlier inject-mode fallback left queued, which then runs once more (a mixed-mode session pays at most one extra review; the queue copy is not cancelled).
|
|
25
27
|
- **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)).
|
|
26
28
|
- **Evidence that cites only bookkeeping frames is REPORTED, never refused (phase 1).** `EVIDENCE_CLASS` (`evolution-plan-validator`) counts the ops whose entire `evidence` list points at turn/step boundary frames — a seq the range rule accepts but that carries no content — and this plugin logs one `dsh-evolution-review:` warning per plan; the count rides `evolution/plan-applied` as `evidenceClassReports` and appears in no model-visible text. Nothing is dropped: `EVIDENCE_RANGE` remains the only evidence gate, and a session log whose frames carry no `seq` leaves the report silent (`undefined` is not an empty index) instead of reporting every op.
|
|
27
29
|
- **`.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). A prompt delivered while a HUMAN message is already queued is not marked at all: the platform claims that human turn first, so a session-level window would attribute the user's own writes to `background_review` (S2-8, FLOW1-3): the plugin reads the pre-claim queue (`agent.inbox`) because the platform exposes no claim identity, and warns once per mount when it withholds the mark.
|
|
@@ -44,9 +46,9 @@ own, and the delivery mode lives on the `evolution-policy` row, not here.
|
|
|
44
46
|
- **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.
|
|
45
47
|
- 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.
|
|
46
48
|
- **`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).
|
|
47
|
-
- **`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
|
|
48
|
-
- **In-flight triggers coalesce, and the drain settles at delivery**: a trigger that arrives while a review is in flight is queued as ONE entry per (session, kind): last trigger wins, so one window can never deliver two prompts for the same segment, and the next completed boundary starts a fresh run once the window closes (the settle wait is bounded: the RESULT watchdog's budget runs from the abort deadline (the full `reviewTimeoutMs` PLUS the 5 s settle margin, the sum capped at the platform timer ceiling), while the `run.dispose` arm point counts that margin alone, from the moment the result settled (PLAN S1.1 / P2-1 / P2-2, 0.3.83); a handle the platform never settles after its abort is abandoned and reported as `evolution/review-error`). A drain delivery that fails restores that session's cadence latch (and, on the completion channel, its `completionInjected` flag), so the segment's review retries at the next completed boundary instead of vanishing with no trace (S2-7, FLOW1-2/1-4).
|
|
49
|
-
- **
|
|
49
|
+
- **`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 turn that runs our notice is excluded from the counting window (an injected review prompt alone must not re-trigger a review under `interval=1`): the platform reports which turn claimed the notice (`agent/inbox/claimed` carries the turn), that turn's `turn/end` settles the window and that turn is not counted, and any unrelated turn ending in the meantime is unaffected (V7-02, S2-9, FLOW1-5).
|
|
50
|
+
- **In-flight triggers coalesce, and the drain settles at delivery**: a trigger that arrives while a review is in flight is queued as ONE entry per (session, kind): last trigger wins, so one window can never deliver two prompts for the same segment, and the next completed boundary starts a fresh run once the window closes (the settle wait is bounded: the RESULT watchdog's budget runs from the abort deadline (the full `reviewTimeoutMs` PLUS the 5 s settle margin, the sum capped at the platform timer ceiling), while the `run.dispose` arm point counts that margin alone, from the moment the result settled (PLAN S1.1 / P2-1 / P2-2, 0.3.83); a handle the platform never settles after its abort is abandoned and reported as `evolution/review-error`). A drain delivery that fails restores that session's cadence latch (and, on the completion channel, its `completionInjected` flag), so the segment's review retries at the next completed boundary instead of vanishing with no trace (S2-7, FLOW1-2/1-4).
|
|
51
|
+
- **One review at a time: the counting window follows the delivered notice, not the delivery.** The `turnsSinceMemory`/`turnsSinceSkill` counters stay monotonic across threshold fires (`resetOnFire: false`) and restart when the notice SETTLES: the queue dropped it unrun (removed, cleared, replaced — `agent/inbox/discarded`) or the turn that claimed it (`agent/inbox/claimed` names the turn) ended. While a notice is outstanding — still queued, or claimed and running — `deliverMessage` refuses to queue another review prompt (it answers `false`, so the caller keeps its one-shot latch and retries at a later boundary); that is what keeps a long task from stacking prompt after prompt. Result notices are not gated. The record is in-memory while the queue is durable, so the judgement reads both: a queue still holding one of our notices with no record (a restart) keeps the window shut, and a record whose notice has left the queue settles as `queue-lost`, while a missed `turn/end` for the claiming turn settles at the next turn that does end (turn numbers increase per session) — a lost event must not wedge the gate. Every settle logs its attribution (`discarded` / `turn-end` / `delivery-no-inbox` / `subagent-flush` / `queue-lost`). 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).
|
|
50
52
|
|
|
51
53
|
**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).
|
|
52
54
|
|
package/lib/index.js
CHANGED
|
@@ -4,6 +4,62 @@ import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
|
4
4
|
import { SessionId } from "@deepseek-ai/dsh-session";
|
|
5
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, collectReadSkillNames, contentHash, evidenceKindIndex, evolutionIoAdapter, filterUnreadSkillOps, filterUnreadSkillOps as filterUnreadSkillOps$1, foldTurn, installParamSection, markReviewChannel, newSkillLibrary, paramNamespace, policyStageLimits, readDispatchSignal, readNumberParam, redactSecrets, resolveOrigins, resolveRootConfig, reviewPrompt, sessionAudited, sweepReviewChannelSessions, verifyPromptBundle } from "@lmzhen/dsh-evolution-core";
|
|
6
6
|
import { validateEvolutionPlan } from "@lmzhen/dsh-evolution-plan-validator";
|
|
7
|
+
/**
|
|
8
|
+
* Whether a message is a notice this plugin delivered, judged only by durable source fields.
|
|
9
|
+
*
|
|
10
|
+
* The source — not the record — is what survives a restart, so a rebuild reads the same facts it
|
|
11
|
+
* would have read live.
|
|
12
|
+
* @param message - the message the platform reported.
|
|
13
|
+
* @returns true only for this plugin's own notices.
|
|
14
|
+
*/
|
|
15
|
+
function isReviewNotice(message) {
|
|
16
|
+
const source = message.source;
|
|
17
|
+
return source?.kind === "plugin" && source.plugin === "dsh-evolution-review" && source.form === "notice";
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Fold one inbox event into the session's outstanding notice.
|
|
21
|
+
*
|
|
22
|
+
* `inserted` and `claimed` ADOPT the message (last wins): a message of ours that the queue holds or
|
|
23
|
+
* a turn is running is outstanding by definition, whether or not a record survived a restart.
|
|
24
|
+
* `discarded` settles whatever we were tracking — or nothing, when the delivery's `inserted` was
|
|
25
|
+
* never seen — and ignores only the stale case: a record naming a DIFFERENT message means the
|
|
26
|
+
* current notice is still queued, so that window must not be reset.
|
|
27
|
+
* @param current - the session's record before this event.
|
|
28
|
+
* @param event - the inbox notification and its message.
|
|
29
|
+
* @returns the record afterwards, and whether the tracked notice settled.
|
|
30
|
+
*/
|
|
31
|
+
function noticeAfter(current, event) {
|
|
32
|
+
if (!isReviewNotice(event.message)) return {
|
|
33
|
+
notice: current,
|
|
34
|
+
settled: false
|
|
35
|
+
};
|
|
36
|
+
switch (event.kind) {
|
|
37
|
+
case "inserted": return {
|
|
38
|
+
notice: {
|
|
39
|
+
messageId: event.message.id,
|
|
40
|
+
turn: null
|
|
41
|
+
},
|
|
42
|
+
settled: false
|
|
43
|
+
};
|
|
44
|
+
case "claimed": return {
|
|
45
|
+
notice: {
|
|
46
|
+
messageId: event.message.id,
|
|
47
|
+
turn: event.turn ?? null
|
|
48
|
+
},
|
|
49
|
+
settled: false
|
|
50
|
+
};
|
|
51
|
+
case "discarded":
|
|
52
|
+
if (current !== void 0 && current.messageId !== event.message.id) return {
|
|
53
|
+
notice: current,
|
|
54
|
+
settled: false
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
notice: void 0,
|
|
58
|
+
settled: true
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
//#endregion
|
|
7
63
|
//#region lib/types/session-state.js
|
|
8
64
|
/**
|
|
9
65
|
* A registry of one plugin instance's session-keyed collections.
|
|
@@ -12,6 +68,9 @@ import { validateEvolutionPlan } from "@lmzhen/dsh-evolution-plan-validator";
|
|
|
12
68
|
* from a hand-written list inside its dispose hook. The list drifted — the
|
|
13
69
|
* 0.3.38-0.3.42 additions (pendingCadenceReviews, pendingCadenceWarned,
|
|
14
70
|
* skipNextCadenceFire, cadenceResetWarned) were missing from it until V7-16 —
|
|
71
|
+
* the first and last of those four have since been replaced by the outstanding-
|
|
72
|
+
* notice window (0.15.2: one record per session, and the claim names the turn), so
|
|
73
|
+
* only pendingCadenceReviews, pendingCadenceWarned and cadenceResetWarned remain —
|
|
15
74
|
* because nothing tied a declaration to its cleanup. Registering at the
|
|
16
75
|
* declaration site removes the list: a collection clears because it was
|
|
17
76
|
* registered, and tests/session-state-ownership.spec.ts fails when a new
|
|
@@ -108,23 +167,9 @@ const REVIEW_OUTPUT_SCHEMA = {
|
|
|
108
167
|
}
|
|
109
168
|
};
|
|
110
169
|
/**
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* pending copy instead of adding a second.
|
|
115
|
-
*/
|
|
116
|
-
function isSameKindPending(message, summary) {
|
|
117
|
-
const source = message.source;
|
|
118
|
-
return source?.kind === "plugin" && source.plugin === "dsh-evolution-review" && source.form === "notice" && source.summary === summary;
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* v43 audit (P1-4 / FLOW1): the cadence summary MUST name the review kind.
|
|
122
|
-
* The three cadence deliveries used to share the bare `auto-review` summary
|
|
123
|
-
* while their prompts differ by kind (memory / skill / combined), so
|
|
124
|
-
* `isSameKindPending` matched ACROSS kinds: a pending memory prompt was replaced
|
|
125
|
-
* in place by a skill prompt — one kind silently lost, while the caller still
|
|
126
|
-
* consumed the cadence latch and the deferred drain still emitted
|
|
127
|
-
* `evolution/review-scheduled` as if both had been delivered.
|
|
170
|
+
* v43 audit (P1-4 / FLOW1): the cadence summary MUST name the review kind — it
|
|
171
|
+
* is what tells the three cadence deliveries apart (their prompts differ by
|
|
172
|
+
* kind: memory / skill / combined), both in the queue and in the session log.
|
|
128
173
|
*/
|
|
129
174
|
const cadenceSummary = (kind) => `auto-review:${kind}`;
|
|
130
175
|
function clampReviewConfig(rawConfig, ctx) {
|
|
@@ -176,12 +221,12 @@ function apply(ctx, rawConfig = {}) {
|
|
|
176
221
|
const turnStarts = sessionState.add("turnStarts", /* @__PURE__ */ new Map());
|
|
177
222
|
let statelessReviewStateWarned = false;
|
|
178
223
|
let channelMarkSuppressedWarned = false;
|
|
224
|
+
let reviewNoticeOutstandingWarned = false;
|
|
179
225
|
const cumulativeToolCalls = sessionState.add("cumulativeToolCalls", /* @__PURE__ */ new Map());
|
|
180
226
|
const completionInjected = sessionState.add("completionInjected", /* @__PURE__ */ new Set());
|
|
181
227
|
const pendingCadenceReviews = sessionState.add("pendingCadenceReviews", /* @__PURE__ */ new Map());
|
|
182
228
|
const pendingCadenceWarned = sessionState.add("pendingCadenceWarned", /* @__PURE__ */ new Set());
|
|
183
|
-
const
|
|
184
|
-
const lastTurnStart = sessionState.add("lastTurnStart", /* @__PURE__ */ new Map());
|
|
229
|
+
const pendingReviewNotices = sessionState.add("pendingReviewNotices", /* @__PURE__ */ new Map());
|
|
185
230
|
const cadenceResetWarned = sessionState.add("cadenceResetWarned", /* @__PURE__ */ new Set());
|
|
186
231
|
let reviewInFlight = false;
|
|
187
232
|
const policy = () => ctx.get("evolutionPolicy")?.get();
|
|
@@ -240,29 +285,83 @@ function apply(ctx, rawConfig = {}) {
|
|
|
240
285
|
}
|
|
241
286
|
ctx.on("session/event", (session, event) => {
|
|
242
287
|
if (!sessionAudited(ctx, session.id, config.sessionScoped)) return;
|
|
243
|
-
if (event.type === "turn/start" && session.header.origin !== "subagent")
|
|
244
|
-
turnStarts.set(session.id, session.seq - 1);
|
|
245
|
-
lastTurnStart.set(session.id, event.data.turn);
|
|
246
|
-
}
|
|
288
|
+
if (event.type === "turn/start" && session.header.origin !== "subagent") turnStarts.set(session.id, session.seq - 1);
|
|
247
289
|
if (event.type === "user/message") {
|
|
248
290
|
if ((event.data?.source)?.kind === "user") clearReviewChannel(session.id);
|
|
249
291
|
return;
|
|
250
292
|
}
|
|
251
293
|
if (event.type !== "turn/end") return;
|
|
252
|
-
if (turnStarts.size >= COUNTER_SWEEP_THRESHOLD || pendingCadenceWarned.size >= COUNTER_SWEEP_THRESHOLD || cumulativeToolCalls.size >= COUNTER_SWEEP_THRESHOLD || completionInjected.size >= COUNTER_SWEEP_THRESHOLD || pendingCadenceReviews.size >= COUNTER_SWEEP_THRESHOLD ||
|
|
294
|
+
if (turnStarts.size >= COUNTER_SWEEP_THRESHOLD || pendingCadenceWarned.size >= COUNTER_SWEEP_THRESHOLD || cumulativeToolCalls.size >= COUNTER_SWEEP_THRESHOLD || completionInjected.size >= COUNTER_SWEEP_THRESHOLD || pendingCadenceReviews.size >= COUNTER_SWEEP_THRESHOLD || cadenceResetWarned.size >= COUNTER_SWEEP_THRESHOLD || pendingReviewNotices.size >= COUNTER_SWEEP_THRESHOLD) {
|
|
253
295
|
const isAlive = (id) => ctx.agents.get(id) !== void 0;
|
|
254
296
|
sweepDeadSessionEntries(turnStarts, isAlive);
|
|
255
|
-
sweepDeadSessionEntries(lastTurnStart, isAlive);
|
|
256
297
|
sweepDeadSessionEntries(cumulativeToolCalls, isAlive);
|
|
257
298
|
sweepDeadSessionEntries(completionInjected, isAlive);
|
|
258
299
|
sweepDeadSessionEntries(pendingCadenceReviews, isAlive);
|
|
259
300
|
sweepDeadSessionEntries(pendingCadenceWarned, isAlive);
|
|
260
|
-
sweepDeadSessionEntries(skipNextCadenceFire, isAlive);
|
|
261
301
|
sweepDeadSessionEntries(cadenceResetWarned, isAlive);
|
|
302
|
+
sweepDeadSessionEntries(pendingReviewNotices, isAlive);
|
|
262
303
|
sweepReviewChannelSessions((id) => ctx.agents.get(SessionId(id)) !== void 0);
|
|
263
304
|
}
|
|
264
305
|
onTurnEnd(session, event);
|
|
265
306
|
});
|
|
307
|
+
/**
|
|
308
|
+
* Settle the session's outstanding notice: drop the record and restart the counting window.
|
|
309
|
+
*
|
|
310
|
+
* The window restarts when the notice LEAVES — either the queue dropped it unrun (`discarded`) or
|
|
311
|
+
* the turn that claimed it ended. The reset runs under the state lock and re-loads the persisted
|
|
312
|
+
* record before zeroing (V24-04): the in-memory snapshot may be turns old by then, and writing it
|
|
313
|
+
* back would roll a later turn's counters away.
|
|
314
|
+
* @param sessionId - the session whose notice settled.
|
|
315
|
+
* @param why - which exit settled it; carried into the warning only, the effect is the same.
|
|
316
|
+
*/
|
|
317
|
+
const settleNotice = async (sessionId, why) => {
|
|
318
|
+
pendingReviewNotices.delete(sessionId);
|
|
319
|
+
ctx.logger.warn(`dsh-evolution-review: the outstanding review notice settled (${why}) — the review window restarts`);
|
|
320
|
+
const stateService = ctx.get("evolutionState");
|
|
321
|
+
try {
|
|
322
|
+
await withReviewStateLock(sessionId, async () => {
|
|
323
|
+
const fresh = await stateService?.loadReviewState(sessionId);
|
|
324
|
+
if (fresh === void 0 || fresh === null) return;
|
|
325
|
+
fresh.turnsSinceMemory = 0;
|
|
326
|
+
fresh.turnsSinceSkill = 0;
|
|
327
|
+
await stateService?.saveReviewState(sessionId, fresh);
|
|
328
|
+
});
|
|
329
|
+
} catch (resetError) {
|
|
330
|
+
if (!cadenceResetWarned.has(sessionId)) {
|
|
331
|
+
cadenceResetWarned.add(sessionId);
|
|
332
|
+
ctx.logger.warn(`dsh-evolution-review: the counting window could not be restarted after a settled review (${why}: ${resetError instanceof Error ? resetError.message : String(resetError)}) — the next boundary may deliver a repeat`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
/**
|
|
337
|
+
* Fold one inbox notification into the session's outstanding-notice record.
|
|
338
|
+
*
|
|
339
|
+
* The platform emits these on the agent scope (plain `ctx.on` receives every agent's, which is
|
|
340
|
+
* how the platform's own plugins consume them); the state machine lives in `review-notice.ts`,
|
|
341
|
+
* so this only routes the payload and keeps the sweep honest.
|
|
342
|
+
* @param kind - which notification arrived.
|
|
343
|
+
* @param payload - the agent, the message and (for a claim) the owning turn.
|
|
344
|
+
*/
|
|
345
|
+
const foldNoticeEvent = (kind, payload) => {
|
|
346
|
+
const sessionId = payload.agent.session.id;
|
|
347
|
+
const outcome = noticeAfter(pendingReviewNotices.get(sessionId), {
|
|
348
|
+
kind,
|
|
349
|
+
message: payload.message,
|
|
350
|
+
turn: payload.turn
|
|
351
|
+
});
|
|
352
|
+
if (outcome.notice === void 0) pendingReviewNotices.delete(sessionId);
|
|
353
|
+
else pendingReviewNotices.set(sessionId, outcome.notice);
|
|
354
|
+
if (outcome.settled) settleNotice(sessionId, "discarded");
|
|
355
|
+
};
|
|
356
|
+
ctx.on("agent/inbox/inserted", (payload) => {
|
|
357
|
+
foldNoticeEvent("inserted", payload);
|
|
358
|
+
});
|
|
359
|
+
ctx.on("agent/inbox/claimed", (payload) => {
|
|
360
|
+
foldNoticeEvent("claimed", payload);
|
|
361
|
+
});
|
|
362
|
+
ctx.on("agent/inbox/discarded", (payload) => {
|
|
363
|
+
foldNoticeEvent("discarded", payload);
|
|
364
|
+
});
|
|
266
365
|
async function onTurnEnd(session, event) {
|
|
267
366
|
try {
|
|
268
367
|
await runOnTurnEnd(session, event);
|
|
@@ -288,20 +387,16 @@ function apply(ctx, rawConfig = {}) {
|
|
|
288
387
|
ctx.logger.warn("dsh-evolution-review: evolution-state service not mounted — memory/skill review cadence is not persisted and resets every turn (see README Known Limitations).");
|
|
289
388
|
}
|
|
290
389
|
const snapshot = policy();
|
|
291
|
-
const
|
|
292
|
-
const
|
|
293
|
-
if (skipFire) if (suppression.turns === void 0 || suppression.turns <= 1) skipNextCadenceFire.delete(session.id);
|
|
294
|
-
else skipNextCadenceFire.set(session.id, {
|
|
295
|
-
afterTurn: suppression.afterTurn,
|
|
296
|
-
turns: suppression.turns - 1
|
|
297
|
-
});
|
|
390
|
+
const outstanding = pendingReviewNotices.get(session.id);
|
|
391
|
+
const settlesHere = outstanding !== void 0 && outstanding.turn !== null && event.data.turn >= outstanding.turn;
|
|
298
392
|
let state = {
|
|
299
393
|
turnsSinceMemory: 0,
|
|
300
394
|
turnsSinceSkill: 0,
|
|
301
395
|
lastTurn: -1
|
|
302
396
|
};
|
|
303
397
|
const advanced = { kind: null };
|
|
304
|
-
await
|
|
398
|
+
if (settlesHere) await settleNotice(session.id, "turn-end");
|
|
399
|
+
else await withReviewStateLock(session.id, async () => {
|
|
305
400
|
state = await stateService?.loadReviewState(session.id) ?? {
|
|
306
401
|
turnsSinceMemory: 0,
|
|
307
402
|
turnsSinceSkill: 0,
|
|
@@ -317,7 +412,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
317
412
|
});
|
|
318
413
|
await stateService?.saveReviewState(session.id, state);
|
|
319
414
|
});
|
|
320
|
-
const kind =
|
|
415
|
+
const kind = advanced.kind;
|
|
321
416
|
const cumulative = (cumulativeToolCalls.get(session.id) ?? 0) + signal.toolCalls;
|
|
322
417
|
cumulativeToolCalls.set(session.id, cumulative);
|
|
323
418
|
if (event.data.reason.kind === "completed") {
|
|
@@ -343,19 +438,21 @@ function apply(ctx, rawConfig = {}) {
|
|
|
343
438
|
}
|
|
344
439
|
} else {
|
|
345
440
|
const reviewOutcome = await trySubagentReview(session, agent, pendingKind, signal);
|
|
346
|
-
if (reviewOutcome === true)
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
441
|
+
if (reviewOutcome === true) {
|
|
442
|
+
try {
|
|
443
|
+
ctx.emit("evolution/review-scheduled", {
|
|
444
|
+
sessionId: session.id,
|
|
445
|
+
kind: pendingKind,
|
|
446
|
+
toolCalls: signal.toolCalls,
|
|
447
|
+
userChars: signal.userChars,
|
|
448
|
+
assistantChars: signal.assistantChars,
|
|
449
|
+
channel: "subagent"
|
|
450
|
+
});
|
|
451
|
+
} catch (emitError) {
|
|
452
|
+
ctx.logger.warn(`dsh-evolution-review: review-scheduled emit failed: ${emitError instanceof Error ? emitError.message : String(emitError)}`);
|
|
453
|
+
}
|
|
454
|
+
await settleNotice(session.id, "subagent-flush");
|
|
455
|
+
} else if (reviewOutcome === "dropped") {
|
|
359
456
|
pendingCadenceReviews.set(session.id, pendingKind);
|
|
360
457
|
return;
|
|
361
458
|
} else if (reviewOutcome !== "deferred") {
|
|
@@ -377,19 +474,6 @@ function apply(ctx, rawConfig = {}) {
|
|
|
377
474
|
}
|
|
378
475
|
}
|
|
379
476
|
}
|
|
380
|
-
try {
|
|
381
|
-
await withReviewStateLock(session.id, async () => {
|
|
382
|
-
const fresh = await stateService?.loadReviewState(session.id) ?? state;
|
|
383
|
-
fresh.turnsSinceMemory = 0;
|
|
384
|
-
fresh.turnsSinceSkill = 0;
|
|
385
|
-
await stateService?.saveReviewState(session.id, fresh);
|
|
386
|
-
});
|
|
387
|
-
} catch (resetError) {
|
|
388
|
-
if (!cadenceResetWarned.has(session.id)) {
|
|
389
|
-
cadenceResetWarned.add(session.id);
|
|
390
|
-
ctx.logger.warn(`dsh-evolution-review: cadence counter reset could not be persisted after a delivered review (${resetError instanceof Error ? resetError.message : String(resetError)}) — a stateful reload may re-deliver this review`);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
477
|
return;
|
|
394
478
|
}
|
|
395
479
|
}
|
|
@@ -401,7 +485,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
401
485
|
}
|
|
402
486
|
return;
|
|
403
487
|
}
|
|
404
|
-
if (
|
|
488
|
+
if (settlesHere) return;
|
|
405
489
|
const trigger = params().skillReviewTrigger;
|
|
406
490
|
if (trigger !== "completion" && trigger !== "both") return;
|
|
407
491
|
if (completionInjected.has(session.id)) return;
|
|
@@ -465,6 +549,42 @@ function apply(ctx, rawConfig = {}) {
|
|
|
465
549
|
* the mark (a host without `inbox` behaves exactly as before).
|
|
466
550
|
*/
|
|
467
551
|
const humanQueuedAhead = (inbox) => [...inbox?.nextTurn ?? [], ...inbox?.nextStep ?? []].some((row) => row.source?.kind === "user");
|
|
552
|
+
/** The one pending row that is a notice of ours, if the host's queue holds one. */
|
|
553
|
+
const queuedNotice = (inbox) => [...inbox?.nextTurn ?? [], ...inbox?.nextStep ?? []].find((row) => isReviewNotice(row));
|
|
554
|
+
/**
|
|
555
|
+
* D 组(兜底):is one of our notices still outstanding? The record is in-memory, the queue is
|
|
556
|
+
* durable, so the queue is read here as well — it is the only account that survives a restart or
|
|
557
|
+
* a mount that missed the live events.
|
|
558
|
+
*
|
|
559
|
+
* Two directions have to hold. A queue holding one of our notices with no record means the review
|
|
560
|
+
* is still waiting (rebuild the record; the `claimed`/`discarded`/`turn/end` pairing then works
|
|
561
|
+
* as usual). A record saying "still queued" while the queue no longer holds that message means the
|
|
562
|
+
* notice left without us seeing the event — settle it, or the gate would stay shut for good.
|
|
563
|
+
* @param sessionId - the session whose window is judged.
|
|
564
|
+
* @param inbox - the agent's pending-message view; without one the record is all there is.
|
|
565
|
+
* @returns true while this session must not be asked for another review.
|
|
566
|
+
*/
|
|
567
|
+
const noticeIsOutstanding = (sessionId, inbox) => {
|
|
568
|
+
const record = pendingReviewNotices.get(sessionId);
|
|
569
|
+
const queued = queuedNotice(inbox);
|
|
570
|
+
if (record === void 0) {
|
|
571
|
+
if (queued === void 0) return false;
|
|
572
|
+
pendingReviewNotices.set(sessionId, {
|
|
573
|
+
messageId: queued.id,
|
|
574
|
+
turn: null
|
|
575
|
+
});
|
|
576
|
+
return true;
|
|
577
|
+
}
|
|
578
|
+
if (record.turn !== null) return true;
|
|
579
|
+
if (queued !== void 0 && queued.id === record.messageId) return true;
|
|
580
|
+
settleNotice(sessionId, "queue-lost");
|
|
581
|
+
if (queued === void 0) return false;
|
|
582
|
+
pendingReviewNotices.set(sessionId, {
|
|
583
|
+
messageId: queued.id,
|
|
584
|
+
turn: null
|
|
585
|
+
});
|
|
586
|
+
return true;
|
|
587
|
+
};
|
|
468
588
|
/** Mark the delivered review prompt's session — unless human input is queued
|
|
469
589
|
* ahead of it (see humanQueuedAhead). Says so once per mount: an unmarked
|
|
470
590
|
* window silently downgrades that review's writes to foreground attribution. */
|
|
@@ -479,6 +599,15 @@ function apply(ctx, rawConfig = {}) {
|
|
|
479
599
|
markReviewChannel(agent.session.id);
|
|
480
600
|
};
|
|
481
601
|
const deliverMessage = (agent, text, summary, reviewPrompt = false) => {
|
|
602
|
+
const wake = agent;
|
|
603
|
+
const inbox = agent.inbox;
|
|
604
|
+
if (reviewPrompt && noticeIsOutstanding(agent.session.id, inbox)) {
|
|
605
|
+
if (!reviewNoticeOutstandingWarned) {
|
|
606
|
+
reviewNoticeOutstandingWarned = true;
|
|
607
|
+
ctx.logger.warn("dsh-evolution-review: this session already has an outstanding review notice (still queued, or claimed by a turn that has not ended) — no second prompt is queued; the review runs when that one settles");
|
|
608
|
+
}
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
482
611
|
const message = createUserMessage({
|
|
483
612
|
content: [{
|
|
484
613
|
type: "text",
|
|
@@ -491,42 +620,11 @@ function apply(ctx, rawConfig = {}) {
|
|
|
491
620
|
summary
|
|
492
621
|
}
|
|
493
622
|
});
|
|
494
|
-
const wake = agent;
|
|
495
|
-
const inbox = agent.inbox;
|
|
496
|
-
if (inbox !== void 0 && typeof inbox.replace === "function") try {
|
|
497
|
-
const supersededTurnRow = (inbox.nextTurn ?? []).find((row) => isSameKindPending(row, summary));
|
|
498
|
-
const superseded = supersededTurnRow ?? (inbox.nextStep ?? []).find((row) => isSameKindPending(row, summary));
|
|
499
|
-
if (superseded !== void 0 && inbox.replace(superseded.id, message)) {
|
|
500
|
-
if (reviewPrompt) markReviewChannelForDelivery(agent, inbox);
|
|
501
|
-
if (supersededTurnRow !== void 0 && typeof wake.followup === "function") {
|
|
502
|
-
wake.followup(createUserMessage({
|
|
503
|
-
content: [{
|
|
504
|
-
type: "text",
|
|
505
|
-
text: `[${summary}] the queued prompt ahead of this notice was refreshed in place; that copy is the current request. If this notice reaches a turn on its own, the review turn already ran — no action is needed.`
|
|
506
|
-
}],
|
|
507
|
-
source: {
|
|
508
|
-
kind: "plugin",
|
|
509
|
-
plugin: "dsh-evolution-review",
|
|
510
|
-
form: "notice",
|
|
511
|
-
summary: `${summary} (wake)`
|
|
512
|
-
}
|
|
513
|
-
}));
|
|
514
|
-
skipNextCadenceFire.set(agent.session.id, {
|
|
515
|
-
afterTurn: lastTurnStart.get(agent.session.id) ?? -1,
|
|
516
|
-
turns: 2
|
|
517
|
-
});
|
|
518
|
-
}
|
|
519
|
-
return true;
|
|
520
|
-
}
|
|
521
|
-
} catch (error) {
|
|
522
|
-
ctx.logger.warn(`dsh-evolution-review: inbox coalescing failed (${error instanceof Error ? error.message : String(error)}) — delivering a fresh message instead`);
|
|
523
|
-
}
|
|
524
623
|
try {
|
|
525
|
-
if (params().reviewWakeInject && typeof wake.followup === "function")
|
|
526
|
-
|
|
527
|
-
skipNextCadenceFire.set(agent.session.id, { afterTurn: lastTurnStart.get(agent.session.id) ?? -1 });
|
|
528
|
-
} else agent.inject(message);
|
|
624
|
+
if (params().reviewWakeInject && typeof wake.followup === "function") wake.followup(message);
|
|
625
|
+
else agent.inject(message);
|
|
529
626
|
if (reviewPrompt) markReviewChannelForDelivery(agent, inbox);
|
|
627
|
+
if (reviewPrompt && inbox === void 0) settleNotice(agent.session.id, "delivery-no-inbox");
|
|
530
628
|
return true;
|
|
531
629
|
} catch (error) {
|
|
532
630
|
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`);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one outstanding review notice of one session, as a pure state machine (A 组).
|
|
3
|
+
*
|
|
4
|
+
* Why this exists: the plugin used to model "have I already asked?" with its own latch and to
|
|
5
|
+
* reset the cadence counter at DELIVERY — three shadows of one fact the platform already owns
|
|
6
|
+
* (the agent's pending-message queue). This module keeps ONE record instead: whether a notice of
|
|
7
|
+
* ours is still outstanding, and which turn took it. The queue's own lifecycle events feed it, so
|
|
8
|
+
* the plugin never invents a second account of the same fact.
|
|
9
|
+
*
|
|
10
|
+
* The events are the agent-scoped inbox notifications: `inserted` (a message entered the queue —
|
|
11
|
+
* which is also what a steer does when it re-inserts the same message into `next-step`),
|
|
12
|
+
* `claimed` (a turn took it, payload carries that turn) and `discarded` (the queue dropped it
|
|
13
|
+
* unrun: removed, cleared, or replaced).
|
|
14
|
+
*
|
|
15
|
+
* Invariants: the record is per session and there is AT MOST ONE (one outstanding review at a
|
|
16
|
+
* time); only the notice we were tracking can settle — an event about some other of our messages
|
|
17
|
+
* leaves the record untouched; nothing here touches counters, io or the locale, which keeps the
|
|
18
|
+
* whole rule testable without a host.
|
|
19
|
+
* @module @lmzhen/dsh-evolution-review/review-notice
|
|
20
|
+
*/
|
|
21
|
+
/** The plugin id every notice of this plugin carries in its durable message source. */
|
|
22
|
+
export declare const REVIEW_NOTICE_PLUGIN = "dsh-evolution-review";
|
|
23
|
+
/** The subset of a platform message this module reads. */
|
|
24
|
+
export interface NoticeMessage {
|
|
25
|
+
readonly id: string;
|
|
26
|
+
readonly source?: {
|
|
27
|
+
readonly kind?: string;
|
|
28
|
+
readonly plugin?: string;
|
|
29
|
+
readonly form?: string;
|
|
30
|
+
} | undefined;
|
|
31
|
+
}
|
|
32
|
+
/** One delivered notice that has not settled yet. */
|
|
33
|
+
export interface ReviewNotice {
|
|
34
|
+
/** The platform identity of the delivered message. */
|
|
35
|
+
readonly messageId: string;
|
|
36
|
+
/** The turn that claimed it, or null while it is still waiting in the queue. */
|
|
37
|
+
readonly turn: number | null;
|
|
38
|
+
}
|
|
39
|
+
/** The three inbox notifications that move a notice from delivery to settlement. */
|
|
40
|
+
export type NoticeEventKind = 'inserted' | 'claimed' | 'discarded';
|
|
41
|
+
/** What one inbox event did to the outstanding notice. */
|
|
42
|
+
export interface NoticeOutcome {
|
|
43
|
+
/** The record that stands afterwards, or undefined when nothing is outstanding. */
|
|
44
|
+
readonly notice: ReviewNotice | undefined;
|
|
45
|
+
/** Whether this event settled the notice we were tracking (the counter resets here). */
|
|
46
|
+
readonly settled: boolean;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Whether a message is a notice this plugin delivered, judged only by durable source fields.
|
|
50
|
+
*
|
|
51
|
+
* The source — not the record — is what survives a restart, so a rebuild reads the same facts it
|
|
52
|
+
* would have read live.
|
|
53
|
+
* @param message - the message the platform reported.
|
|
54
|
+
* @returns true only for this plugin's own notices.
|
|
55
|
+
*/
|
|
56
|
+
export declare function isReviewNotice(message: NoticeMessage): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Fold one inbox event into the session's outstanding notice.
|
|
59
|
+
*
|
|
60
|
+
* `inserted` and `claimed` ADOPT the message (last wins): a message of ours that the queue holds or
|
|
61
|
+
* a turn is running is outstanding by definition, whether or not a record survived a restart.
|
|
62
|
+
* `discarded` settles whatever we were tracking — or nothing, when the delivery's `inserted` was
|
|
63
|
+
* never seen — and ignores only the stale case: a record naming a DIFFERENT message means the
|
|
64
|
+
* current notice is still queued, so that window must not be reset.
|
|
65
|
+
* @param current - the session's record before this event.
|
|
66
|
+
* @param event - the inbox notification and its message.
|
|
67
|
+
* @returns the record afterwards, and whether the tracked notice settled.
|
|
68
|
+
*/
|
|
69
|
+
export declare function noticeAfter(current: ReviewNotice | undefined, event: {
|
|
70
|
+
readonly kind: NoticeEventKind;
|
|
71
|
+
readonly message: NoticeMessage;
|
|
72
|
+
readonly turn?: number | undefined;
|
|
73
|
+
}): NoticeOutcome;
|
|
74
|
+
//# sourceMappingURL=review-notice.d.ts.map
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
* from a hand-written list inside its dispose hook. The list drifted — the
|
|
6
6
|
* 0.3.38-0.3.42 additions (pendingCadenceReviews, pendingCadenceWarned,
|
|
7
7
|
* skipNextCadenceFire, cadenceResetWarned) were missing from it until V7-16 —
|
|
8
|
+
* the first and last of those four have since been replaced by the outstanding-
|
|
9
|
+
* notice window (0.15.2: one record per session, and the claim names the turn), so
|
|
10
|
+
* only pendingCadenceReviews, pendingCadenceWarned and cadenceResetWarned remain —
|
|
8
11
|
* because nothing tied a declaration to its cleanup. Registering at the
|
|
9
12
|
* declaration site removes the list: a collection clears because it was
|
|
10
13
|
* registered, and tests/session-state-ownership.spec.ts fails when a new
|
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.16.0",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
30
|
-
"@lmzhen/dsh-evolution-approval": "^0.
|
|
31
|
-
"@lmzhen/dsh-evolution-core": "^0.
|
|
32
|
-
"@lmzhen/dsh-evolution-plan-validator": "^0.
|
|
30
|
+
"@lmzhen/dsh-evolution-approval": "^0.16.0",
|
|
31
|
+
"@lmzhen/dsh-evolution-core": "^0.16.0",
|
|
32
|
+
"@lmzhen/dsh-evolution-plan-validator": "^0.16.0"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
|
|
38
38
|
"@deepseek-ai/dsh-session": "^0.1.5-rc.2",
|
|
39
39
|
"@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
|
|
40
|
-
"@lmzhen/dsh-evolution-state": "^0.
|
|
41
|
-
"@lmzhen/dsh-evolution-policy": "^0.
|
|
40
|
+
"@lmzhen/dsh-evolution-state": "^0.16.0",
|
|
41
|
+
"@lmzhen/dsh-evolution-policy": "^0.16.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@deepseek-ai/dsh-agent": "^0.1.5-rc.2",
|
|
@@ -48,10 +48,10 @@
|
|
|
48
48
|
"@deepseek-ai/dsh-session-persistence": "^0.1.5-rc.2",
|
|
49
49
|
"@deepseek-ai/dsh-session-persistence-jsonl": "^0.1.5-rc.2",
|
|
50
50
|
"@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
|
|
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.16.0",
|
|
52
|
+
"@lmzhen/dsh-evolution-core": "^0.16.0",
|
|
53
|
+
"@lmzhen/dsh-evolution-curator": "^0.16.0",
|
|
54
|
+
"@lmzhen/dsh-evolution-plan-validator": "^0.16.0",
|
|
55
|
+
"@lmzhen/dsh-evolution-state": "^0.16.0"
|
|
56
56
|
}
|
|
57
57
|
}
|