@lmzhen/dsh-evolution-review 0.3.81 → 0.3.83
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 +6 -5
- package/lib/index.js +217 -41
- package/lib/types/index.d.ts +7 -0
- package/lib/types/session-state.d.ts +36 -0
- package/package.json +11 -11
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @
|
|
1
|
+
# @lmzhen/dsh-evolution-review
|
|
2
2
|
|
|
3
3
|
Background review orchestration
|
|
4
4
|
|
|
@@ -8,7 +8,7 @@ Background review orchestration
|
|
|
8
8
|
|
|
9
9
|
#### What the model sees
|
|
10
10
|
|
|
11
|
-
`@
|
|
11
|
+
`@lmzhen/dsh-evolution-review` registers no direct prompt or tool schema itself. Model-visible effects are owned by the packages that consume this service.
|
|
12
12
|
|
|
13
13
|
#### Token effect
|
|
14
14
|
|
|
@@ -31,19 +31,20 @@ Independent of request-prefix construction. This package does not alter the asse
|
|
|
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
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)).
|
|
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)). 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.
|
|
35
35
|
|
|
36
36
|
## Configuration
|
|
37
37
|
|
|
38
38
|
`reviewProvider` selects the LLM provider for review subagents. When omitted, the subagent inherits the deployment default route instead of a hardcoded provider name. Model selection stays on the policy (`memoryReviewModel` / `skillReviewModel`).
|
|
39
39
|
|
|
40
|
-
`reviewTimeoutMs` bounds each review subagent run (an `AbortSignal.timeout`; `0` aborts immediately). The former `executionTimeoutMs` declaration was removed in v14 (nothing read it, so it was configuration that did nothing); use `reviewTimeoutMs`.
|
|
40
|
+
`reviewTimeoutMs` bounds each review subagent run (an `AbortSignal.timeout`; `0` aborts immediately) and the write leg that executes the plan. The deadline does not stop an abandoned write leg: an op that lands after it is reported to the model as its own notice (S2-10, FLOW1-6), while the `evolution/plan-applied` record stays the deadline snapshot, so late writes still have no plan-applied entry. The former `executionTimeoutMs` declaration was removed in v14 (nothing read it, so it was configuration that did nothing); use `reviewTimeoutMs`.
|
|
41
41
|
|
|
42
42
|
### Review delivery contract (0.3.38-0.3.42)
|
|
43
43
|
|
|
44
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.
|
|
45
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).
|
|
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
|
|
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 (an injected review prompt alone must not re-trigger a review under `interval=1`): a delivery that appends suppresses ONE turn, and an inbox-replace re-armed wake suppresses the TWO turns it can produce — the refreshed-prompt turn, then the wake-stub turn queued behind it — because the platform claims one next-turn per driver round (PLAN-R2 P1-1, 2026-09-16); the suppression is bound to the turn(s) the delivery WOKE — a busy-period turn that started before the delivery keeps its own cadence and cannot consume it (S2-9, FLOW1-5: the platform's `turn/start` carries only `{ turn }`, so ordering is the binding identity); a restart clears the queue, so the loop cannot survive it.
|
|
47
|
+
- **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; 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).
|
|
47
48
|
- **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
|
|
|
49
50
|
**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
|
@@ -4,11 +4,67 @@ 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, contentHash, evolutionIoAdapter, foldToolDispatches, foldTurn, markReviewChannel, newSkillLibrary, readDispatchSignal, redactSecrets, resolveOrigins, resolveRootConfig, reviewPrompt, sessionAudited, skillReadNameOf, sweepReviewChannelSessions, verifyPromptBundle } from "@lmzhen/dsh-evolution-core";
|
|
6
6
|
import { validateEvolutionPlan } from "@lmzhen/dsh-evolution-plan-validator";
|
|
7
|
+
//#region lib/types/session-state.js
|
|
8
|
+
/**
|
|
9
|
+
* A registry of one plugin instance's session-keyed collections.
|
|
10
|
+
*
|
|
11
|
+
* P1-9 (S2-2): the review plugin kept eight per-session maps and cleared them
|
|
12
|
+
* from a hand-written list inside its dispose hook. The list drifted — the
|
|
13
|
+
* 0.3.38-0.3.42 additions (pendingCadenceReviews, pendingCadenceWarned,
|
|
14
|
+
* skipNextCadenceFire, cadenceResetWarned) were missing from it until V7-16 —
|
|
15
|
+
* because nothing tied a declaration to its cleanup. Registering at the
|
|
16
|
+
* declaration site removes the list: a collection clears because it was
|
|
17
|
+
* registered, and tests/session-state-ownership.spec.ts fails when a new
|
|
18
|
+
* `new Map<SessionId, …>` skips the registry.
|
|
19
|
+
*
|
|
20
|
+
* Deliberately package-local: evolution-review is the only current consumer
|
|
21
|
+
* (commands holds no per-session state, and state-json's warn dedupe sets are
|
|
22
|
+
* process-lifetime by design). Promote it to evolution-core when a second
|
|
23
|
+
* package needs the same registration discipline.
|
|
24
|
+
*/
|
|
25
|
+
var SessionScopedState = class {
|
|
26
|
+
registered = [];
|
|
27
|
+
/**
|
|
28
|
+
* Register a collection and return it, so declaration and registration stay
|
|
29
|
+
* one statement.
|
|
30
|
+
*
|
|
31
|
+
* @param key - the declaring variable name; also the diagnostic name.
|
|
32
|
+
* @param collection - any collection exposing clear().
|
|
33
|
+
* @returns the same collection.
|
|
34
|
+
*/
|
|
35
|
+
add(key, collection) {
|
|
36
|
+
if (this.registered.some((entry) => entry.key === key)) throw new Error(`dsh-evolution-review: session state "${key}" is registered twice`);
|
|
37
|
+
this.registered.push({
|
|
38
|
+
key,
|
|
39
|
+
collection
|
|
40
|
+
});
|
|
41
|
+
return collection;
|
|
42
|
+
}
|
|
43
|
+
/** Registered keys in registration order (diagnostics and the ownership spec). */
|
|
44
|
+
keys() {
|
|
45
|
+
return this.registered.map((entry) => entry.key);
|
|
46
|
+
}
|
|
47
|
+
/** Clear every registered collection. Idempotent; safe on a partially used mount. */
|
|
48
|
+
dispose() {
|
|
49
|
+
for (const entry of this.registered) entry.collection.clear();
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
//#endregion
|
|
7
53
|
//#region lib/types/index.js
|
|
8
54
|
/**
|
|
9
55
|
* Background review orchestration: signal gate → one-shot subagent → trusted plan execution.
|
|
10
56
|
* @module @lmzhen/dsh-evolution-review
|
|
11
57
|
*/
|
|
58
|
+
/** S2-6 (FLOW1-1): the settle-grace margin a subagent handle gets beyond its
|
|
59
|
+
* own deadline before the review abandons it. As the dispose watchdog's WHOLE
|
|
60
|
+
* budget it is capped by the review timeout so a short (test) budget stays
|
|
61
|
+
* short; the result watchdog adds it AFTER the full timeout (PLAN S1.1,
|
|
62
|
+
* 2026-09-16) and caps the sum at the timer ceiling (PLAN-R2 P2-1). The
|
|
63
|
+
* `run.dispose` arm point counts this margin ALONE — its clock starts after
|
|
64
|
+
* the result settled, not at the review timeout (PLAN-R2 P2-2, 2026-09-16). */
|
|
65
|
+
const REVIEW_SETTLE_MARGIN_MS = 5e3;
|
|
66
|
+
/** Error name marking the S2-6 watchdog expiry (see the catch in trySubagentReview). */
|
|
67
|
+
const REVIEW_SETTLE_TIMEOUT = "ReviewSettleTimeout";
|
|
12
68
|
const name = "evolution-review";
|
|
13
69
|
const inject = ["agents"];
|
|
14
70
|
const Config = z.object({
|
|
@@ -61,6 +117,16 @@ function isSameKindPending(message, summary) {
|
|
|
61
117
|
const source = message.source;
|
|
62
118
|
return source?.kind === "plugin" && source.plugin === "dsh-evolution-review" && source.form === "notice" && source.summary === summary;
|
|
63
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.
|
|
128
|
+
*/
|
|
129
|
+
const cadenceSummary = (kind) => `auto-review:${kind}`;
|
|
64
130
|
function clampReviewConfig(rawConfig, ctx) {
|
|
65
131
|
const clamped = [];
|
|
66
132
|
const field = (name, value, fallback, min, max) => {
|
|
@@ -94,14 +160,17 @@ function apply(ctx, rawConfig = {}) {
|
|
|
94
160
|
const config = clampReviewConfig(rawConfig, ctx);
|
|
95
161
|
assertSkillsRootAliasRetired(rawConfig);
|
|
96
162
|
const rootConfig = resolveRootConfig(rawConfig);
|
|
97
|
-
const
|
|
163
|
+
const sessionState = new SessionScopedState();
|
|
164
|
+
const turnStarts = sessionState.add("turnStarts", /* @__PURE__ */ new Map());
|
|
98
165
|
let statelessReviewStateWarned = false;
|
|
99
|
-
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
const
|
|
166
|
+
let channelMarkSuppressedWarned = false;
|
|
167
|
+
const cumulativeToolCalls = sessionState.add("cumulativeToolCalls", /* @__PURE__ */ new Map());
|
|
168
|
+
const completionInjected = sessionState.add("completionInjected", /* @__PURE__ */ new Set());
|
|
169
|
+
const pendingCadenceReviews = sessionState.add("pendingCadenceReviews", /* @__PURE__ */ new Map());
|
|
170
|
+
const pendingCadenceWarned = sessionState.add("pendingCadenceWarned", /* @__PURE__ */ new Set());
|
|
171
|
+
const skipNextCadenceFire = sessionState.add("skipNextCadenceFire", /* @__PURE__ */ new Map());
|
|
172
|
+
const lastTurnStart = sessionState.add("lastTurnStart", /* @__PURE__ */ new Map());
|
|
173
|
+
const cadenceResetWarned = sessionState.add("cadenceResetWarned", /* @__PURE__ */ new Set());
|
|
105
174
|
let reviewInFlight = false;
|
|
106
175
|
const policy = () => ctx.get("evolutionPolicy")?.get();
|
|
107
176
|
const schemaDefaults = Config["~standard"].validate({}).value;
|
|
@@ -122,8 +191,8 @@ function apply(ctx, rawConfig = {}) {
|
|
|
122
191
|
warnShadowed();
|
|
123
192
|
});
|
|
124
193
|
}
|
|
125
|
-
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
|
|
126
|
-
const reviewStateLocks = /* @__PURE__ */ new Map();
|
|
194
|
+
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 the evolution-review README, \"Known Limitations and Deferred Work\"). Set reviewMode: \"subagent\" on the evolution-policy row to keep the audited plan path.");
|
|
195
|
+
const reviewStateLocks = sessionState.add("reviewStateLocks", /* @__PURE__ */ new Map());
|
|
127
196
|
async function withReviewStateLock(id, task) {
|
|
128
197
|
const next = (reviewStateLocks.get(id) ?? Promise.resolve()).catch(() => {}).then(task);
|
|
129
198
|
reviewStateLocks.set(id, next);
|
|
@@ -135,15 +204,19 @@ function apply(ctx, rawConfig = {}) {
|
|
|
135
204
|
}
|
|
136
205
|
ctx.on("session/event", (session, event) => {
|
|
137
206
|
if (!sessionAudited(ctx, session.id, config.sessionScoped)) return;
|
|
138
|
-
if (event.type === "turn/start" && session.header.origin !== "subagent")
|
|
207
|
+
if (event.type === "turn/start" && session.header.origin !== "subagent") {
|
|
208
|
+
turnStarts.set(session.id, session.seq - 1);
|
|
209
|
+
lastTurnStart.set(session.id, event.data.turn);
|
|
210
|
+
}
|
|
139
211
|
if (event.type === "user/message") {
|
|
140
212
|
if ((event.data?.source)?.kind === "user") clearReviewChannel(session.id);
|
|
141
213
|
return;
|
|
142
214
|
}
|
|
143
215
|
if (event.type !== "turn/end") return;
|
|
144
|
-
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) {
|
|
216
|
+
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 || lastTurnStart.size >= COUNTER_SWEEP_THRESHOLD) {
|
|
145
217
|
const isAlive = (id) => ctx.agents.get(id) !== void 0;
|
|
146
218
|
sweepDeadSessionEntries(turnStarts, isAlive);
|
|
219
|
+
sweepDeadSessionEntries(lastTurnStart, isAlive);
|
|
147
220
|
sweepDeadSessionEntries(cumulativeToolCalls, isAlive);
|
|
148
221
|
sweepDeadSessionEntries(completionInjected, isAlive);
|
|
149
222
|
sweepDeadSessionEntries(pendingCadenceReviews, isAlive);
|
|
@@ -179,8 +252,13 @@ function apply(ctx, rawConfig = {}) {
|
|
|
179
252
|
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).");
|
|
180
253
|
}
|
|
181
254
|
const snapshot = policy();
|
|
182
|
-
const
|
|
183
|
-
|
|
255
|
+
const suppression = skipNextCadenceFire.get(session.id);
|
|
256
|
+
const skipFire = suppression !== void 0 && event.data.turn > suppression.afterTurn;
|
|
257
|
+
if (skipFire) if (suppression.turns === void 0 || suppression.turns <= 1) skipNextCadenceFire.delete(session.id);
|
|
258
|
+
else skipNextCadenceFire.set(session.id, {
|
|
259
|
+
afterTurn: suppression.afterTurn,
|
|
260
|
+
turns: suppression.turns - 1
|
|
261
|
+
});
|
|
184
262
|
let state = {
|
|
185
263
|
turnsSinceMemory: 0,
|
|
186
264
|
turnsSinceSkill: 0,
|
|
@@ -211,7 +289,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
211
289
|
if (pendingKind !== void 0) {
|
|
212
290
|
pendingCadenceReviews.delete(session.id);
|
|
213
291
|
if ((policy()?.reviewMode ?? config.reviewMode) === "inject") {
|
|
214
|
-
if (!deliverMessage(agent, reviewPrompt(pendingKind),
|
|
292
|
+
if (!deliverMessage(agent, reviewPrompt(pendingKind), cadenceSummary(pendingKind), true)) {
|
|
215
293
|
pendingCadenceReviews.set(session.id, pendingKind);
|
|
216
294
|
return;
|
|
217
295
|
}
|
|
@@ -245,7 +323,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
245
323
|
pendingCadenceReviews.set(session.id, pendingKind);
|
|
246
324
|
return;
|
|
247
325
|
} else if (reviewOutcome !== "deferred") {
|
|
248
|
-
if (!deliverMessage(agent, reviewPrompt(pendingKind),
|
|
326
|
+
if (!deliverMessage(agent, reviewPrompt(pendingKind), cadenceSummary(pendingKind), true)) {
|
|
249
327
|
pendingCadenceReviews.set(session.id, pendingKind);
|
|
250
328
|
return;
|
|
251
329
|
}
|
|
@@ -339,6 +417,31 @@ function apply(ctx, rawConfig = {}) {
|
|
|
339
417
|
* prototype method), the caller's catch demoted that to a console warning,
|
|
340
418
|
* and the cadence reset ran anyway — the segment's review was consumed with
|
|
341
419
|
* nothing queued (silent no-delivery window: 2026-09-07 → 0.3.73). */
|
|
420
|
+
/**
|
|
421
|
+
* S2-8 (FLOW1-3): is a HUMAN message already queued ahead of ours? The
|
|
422
|
+
* review-channel mark is a session-level WINDOW, not a claim on one message:
|
|
423
|
+
* if a real user message is pending when the review prompt is delivered, the
|
|
424
|
+
* platform claims that human turn first and the mark would be in effect for
|
|
425
|
+
* the user's own writes — attributing them `background_review` (the pinned /
|
|
426
|
+
* hermes-managed protection would cover human edits). The platform exposes no
|
|
427
|
+
* claim identity to the plugin, so the pre-claim queue is the identity that is
|
|
428
|
+
* readable; this is the plan's documented fallback, and it only ever WITHHOLDS
|
|
429
|
+
* the mark (a host without `inbox` behaves exactly as before).
|
|
430
|
+
*/
|
|
431
|
+
const humanQueuedAhead = (inbox) => [...inbox?.nextTurn ?? [], ...inbox?.nextStep ?? []].some((row) => row.source?.kind === "user");
|
|
432
|
+
/** Mark the delivered review prompt's session — unless human input is queued
|
|
433
|
+
* ahead of it (see humanQueuedAhead). Says so once per mount: an unmarked
|
|
434
|
+
* window silently downgrades that review's writes to foreground attribution. */
|
|
435
|
+
const markReviewChannelForDelivery = (agent, inbox) => {
|
|
436
|
+
if (humanQueuedAhead(inbox)) {
|
|
437
|
+
if (!channelMarkSuppressedWarned) {
|
|
438
|
+
channelMarkSuppressedWarned = true;
|
|
439
|
+
ctx.logger.warn("dsh-evolution-review: the review prompt was delivered BEHIND queued human input — the review-channel mark stays unset for this window, so this review’s writes are attributed foreground and the .pinned/.hermes-managed protection does not cover them");
|
|
440
|
+
}
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
markReviewChannel(agent.session.id);
|
|
444
|
+
};
|
|
342
445
|
const deliverMessage = (agent, text, summary, reviewPrompt = false) => {
|
|
343
446
|
const message = createUserMessage({
|
|
344
447
|
content: [{
|
|
@@ -352,29 +455,73 @@ function apply(ctx, rawConfig = {}) {
|
|
|
352
455
|
summary
|
|
353
456
|
}
|
|
354
457
|
});
|
|
458
|
+
const wake = agent;
|
|
355
459
|
const inbox = agent.inbox;
|
|
356
460
|
if (inbox !== void 0 && typeof inbox.replace === "function") try {
|
|
357
|
-
const
|
|
461
|
+
const supersededTurnRow = (inbox.nextTurn ?? []).find((row) => isSameKindPending(row, summary));
|
|
462
|
+
const superseded = supersededTurnRow ?? (inbox.nextStep ?? []).find((row) => isSameKindPending(row, summary));
|
|
358
463
|
if (superseded !== void 0 && inbox.replace(superseded.id, message)) {
|
|
359
|
-
if (reviewPrompt)
|
|
464
|
+
if (reviewPrompt) markReviewChannelForDelivery(agent, inbox);
|
|
465
|
+
if (supersededTurnRow !== void 0 && typeof wake.followup === "function") {
|
|
466
|
+
wake.followup(createUserMessage({
|
|
467
|
+
content: [{
|
|
468
|
+
type: "text",
|
|
469
|
+
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.`
|
|
470
|
+
}],
|
|
471
|
+
source: {
|
|
472
|
+
kind: "plugin",
|
|
473
|
+
plugin: "dsh-evolution-review",
|
|
474
|
+
form: "notice",
|
|
475
|
+
summary: `${summary} (wake)`
|
|
476
|
+
}
|
|
477
|
+
}));
|
|
478
|
+
skipNextCadenceFire.set(agent.session.id, {
|
|
479
|
+
afterTurn: lastTurnStart.get(agent.session.id) ?? -1,
|
|
480
|
+
turns: 2
|
|
481
|
+
});
|
|
482
|
+
}
|
|
360
483
|
return true;
|
|
361
484
|
}
|
|
362
485
|
} catch (error) {
|
|
363
486
|
ctx.logger.warn(`dsh-evolution-review: inbox coalescing failed (${error instanceof Error ? error.message : String(error)}) — delivering a fresh message instead`);
|
|
364
487
|
}
|
|
365
|
-
const wake = agent;
|
|
366
488
|
try {
|
|
367
489
|
if (config.reviewWakeInject && typeof wake.followup === "function") {
|
|
368
490
|
wake.followup(message);
|
|
369
|
-
skipNextCadenceFire.set(agent.session.id,
|
|
491
|
+
skipNextCadenceFire.set(agent.session.id, { afterTurn: lastTurnStart.get(agent.session.id) ?? -1 });
|
|
370
492
|
} else agent.inject(message);
|
|
371
|
-
if (reviewPrompt)
|
|
493
|
+
if (reviewPrompt) markReviewChannelForDelivery(agent, inbox);
|
|
372
494
|
return true;
|
|
373
495
|
} catch (error) {
|
|
374
496
|
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`);
|
|
375
497
|
return false;
|
|
376
498
|
}
|
|
377
499
|
};
|
|
500
|
+
let settleBudgetCapWarned = false;
|
|
501
|
+
const resultSettleBudgetMs = () => {
|
|
502
|
+
const budget = config.reviewTimeoutMs + Math.min(REVIEW_SETTLE_MARGIN_MS, config.reviewTimeoutMs);
|
|
503
|
+
if (budget <= MAX_TIMER_DELAY_MS) return budget;
|
|
504
|
+
if (!settleBudgetCapWarned) {
|
|
505
|
+
settleBudgetCapWarned = true;
|
|
506
|
+
ctx.logger.warn(`dsh-evolution-review: reviewTimeoutMs ${config.reviewTimeoutMs}ms plus the settle margin exceeds the 32-bit timer delay ceiling (${MAX_TIMER_DELAY_MS}ms) — the settle watchdog arms at the ceiling`);
|
|
507
|
+
}
|
|
508
|
+
return MAX_TIMER_DELAY_MS;
|
|
509
|
+
};
|
|
510
|
+
const disposeSettleBudgetMs = () => Math.min(REVIEW_SETTLE_MARGIN_MS, config.reviewTimeoutMs);
|
|
511
|
+
const withSettleWatchdog = (promise, label, budgetMs, overdue) => new Promise((resolve, reject) => {
|
|
512
|
+
const timer = setTimeout(() => {
|
|
513
|
+
const error = /* @__PURE__ */ new Error(`dsh-evolution-review: ${label} ${overdue(budgetMs)} — abandoning the handle`);
|
|
514
|
+
error.name = REVIEW_SETTLE_TIMEOUT;
|
|
515
|
+
reject(error);
|
|
516
|
+
}, budgetMs);
|
|
517
|
+
promise.then((value) => {
|
|
518
|
+
clearTimeout(timer);
|
|
519
|
+
resolve(value);
|
|
520
|
+
}, (error) => {
|
|
521
|
+
clearTimeout(timer);
|
|
522
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
523
|
+
});
|
|
524
|
+
});
|
|
378
525
|
const withTimeout = (promise, ms, label) => new Promise((resolve, reject) => {
|
|
379
526
|
const timer = setTimeout(() => {
|
|
380
527
|
reject(/* @__PURE__ */ new Error(`dsh-evolution-review: ${label} timed out after ${ms}ms`));
|
|
@@ -417,16 +564,19 @@ function apply(ctx, rawConfig = {}) {
|
|
|
417
564
|
const subagents = ctx.get("subagents");
|
|
418
565
|
if (!subagents) return false;
|
|
419
566
|
if (reviewInFlight) {
|
|
420
|
-
|
|
421
|
-
|
|
567
|
+
const existing = deferredFallbackReviews.findIndex((entry) => entry.sessionId === session.id && entry.kind === kind);
|
|
568
|
+
if (existing >= 0 || deferredFallbackReviews.length < DEFERRED_REVIEW_CAP) {
|
|
569
|
+
const entry = {
|
|
422
570
|
agent,
|
|
423
571
|
sessionId: session.id,
|
|
424
572
|
kind,
|
|
425
573
|
prompt: reviewPrompt(kind),
|
|
426
|
-
label:
|
|
574
|
+
label: cadenceSummary(kind),
|
|
427
575
|
channel: "inject",
|
|
428
576
|
counts: signal
|
|
429
|
-
}
|
|
577
|
+
};
|
|
578
|
+
if (existing >= 0) deferredFallbackReviews[existing] = entry;
|
|
579
|
+
else deferredFallbackReviews.push(entry);
|
|
430
580
|
return "deferred";
|
|
431
581
|
}
|
|
432
582
|
ctx.logger.warn(`dsh-evolution-review: deferred-review queue at cap (${DEFERRED_REVIEW_CAP}) — dropping one fallback review prompt`);
|
|
@@ -463,7 +613,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
463
613
|
outputSchema: REVIEW_OUTPUT_SCHEMA
|
|
464
614
|
});
|
|
465
615
|
try {
|
|
466
|
-
const result = await run.result;
|
|
616
|
+
const result = await withSettleWatchdog(run.result, "subagent review result", resultSettleBudgetMs(), (total) => `did not settle within ${Math.max(0, total - config.reviewTimeoutMs)}ms after the review timeout (watchdog total ${total}ms)`);
|
|
467
617
|
if (!result.structured) {
|
|
468
618
|
try {
|
|
469
619
|
ctx.emit("evolution/review-error", { sessionId: session.id });
|
|
@@ -509,10 +659,21 @@ function apply(ctx, rawConfig = {}) {
|
|
|
509
659
|
}
|
|
510
660
|
};
|
|
511
661
|
const landed = [];
|
|
662
|
+
let deadlineMissed = false;
|
|
663
|
+
const lateLandings = [];
|
|
664
|
+
const noteLanded = (action) => {
|
|
665
|
+
landed.push(action);
|
|
666
|
+
if (!deadlineMissed) return;
|
|
667
|
+
lateLandings.push(action);
|
|
668
|
+
const before = landed.length - lateLandings.length;
|
|
669
|
+
const earlier = before > 0 ? ` (${before} other op(s) had landed before the deadline)` : "";
|
|
670
|
+
deliverMessage(agent, `\u{1F4BE} Self-improvement review: ${action} ${lateLandings.length === 1 ? "landed AFTER the review timed out" : `landed after the timeout (${lateLandings.length} late so far)`}${earlier} \u2014 reconcile before re-requesting the same change.`, "self-improvement review late landing");
|
|
671
|
+
};
|
|
512
672
|
let executed;
|
|
513
673
|
try {
|
|
514
|
-
executed = await withTimeout(executePlan(validation.accepted, session,
|
|
674
|
+
executed = await withTimeout(executePlan(validation.accepted, session, noteLanded, preRunHashes), config.reviewTimeoutMs, "review plan execution");
|
|
515
675
|
} catch (error) {
|
|
676
|
+
deadlineMissed = true;
|
|
516
677
|
emitApplied({
|
|
517
678
|
actions: landed,
|
|
518
679
|
executionError: `execution timed out after ${config.reviewTimeoutMs}ms`
|
|
@@ -543,14 +704,19 @@ function apply(ctx, rawConfig = {}) {
|
|
|
543
704
|
return true;
|
|
544
705
|
} finally {
|
|
545
706
|
try {
|
|
546
|
-
await run.dispose();
|
|
707
|
+
await withSettleWatchdog(run.dispose(), "subagent dispose", disposeSettleBudgetMs(), (total) => `did not settle within ${total}ms`);
|
|
547
708
|
} catch (disposeError) {
|
|
548
709
|
ctx.logger.warn(`dsh-evolution-review: subagent dispose failed: ${disposeError instanceof Error ? disposeError.message : String(disposeError)}`);
|
|
549
710
|
}
|
|
550
711
|
}
|
|
551
712
|
} catch (error) {
|
|
552
713
|
ctx.logger.warn(`dsh-evolution-review: subagent review failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
553
|
-
if (error instanceof Error && error.
|
|
714
|
+
if (error instanceof Error && error.name === REVIEW_SETTLE_TIMEOUT) try {
|
|
715
|
+
ctx.emit("evolution/review-error", { sessionId: session.id });
|
|
716
|
+
} catch (emitError) {
|
|
717
|
+
ctx.logger.warn(`dsh-evolution-review: review-error emit failed: ${emitError instanceof Error ? emitError.message : String(emitError)}`);
|
|
718
|
+
}
|
|
719
|
+
if (error instanceof Error && error.message.includes("plan execution timed out")) ctx.logger.warn("dsh-evolution-review: plan execution abandoned on timeout — late writes ARE reported to the model as they land (S2-10) but still have NO plan-applied record and race the fallback inject; inspect the skill tree and usage sidecar");
|
|
554
720
|
return false;
|
|
555
721
|
} finally {
|
|
556
722
|
reviewInFlight = false;
|
|
@@ -558,6 +724,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
558
724
|
for (const { agent: waitingAgent, sessionId: entrySession, kind: waitingKind, prompt, label, channel, counts: entryCounts } of deferred) {
|
|
559
725
|
if (!deliverMessage(waitingAgent, prompt, label, true)) {
|
|
560
726
|
if (channel === "completion") completionInjected.delete(entrySession);
|
|
727
|
+
else pendingCadenceReviews.set(entrySession, waitingKind);
|
|
561
728
|
continue;
|
|
562
729
|
}
|
|
563
730
|
try {
|
|
@@ -757,12 +924,12 @@ function apply(ctx, rawConfig = {}) {
|
|
|
757
924
|
if (op.action === "edit" || op.action === "update") {
|
|
758
925
|
const updated = await library.update(name, op.content ?? "", origin, anchorOf(op));
|
|
759
926
|
if (updated.stale === true) return staleRefusal(updated, name, op.file_path);
|
|
760
|
-
if (updated.ok) await ctx.get("skillUsage")?.record?.(name, "patch").catch(() => {});
|
|
927
|
+
if (updated.ok && updated.noop !== true) await ctx.get("skillUsage")?.record?.(name, "patch").catch(() => {});
|
|
761
928
|
return updated;
|
|
762
929
|
}
|
|
763
930
|
if (op.action === "patch") {
|
|
764
931
|
const patched = await library.patch(name, op.old_string ?? "", op.new_string ?? "", op.file_path ?? "", op.replace_all === true, origin);
|
|
765
|
-
if (patched.ok) await ctx.get("skillUsage")?.record?.(name, "patch").catch(() => {});
|
|
932
|
+
if (patched.ok && patched.noop !== true) await ctx.get("skillUsage")?.record?.(name, "patch").catch(() => {});
|
|
766
933
|
return patched;
|
|
767
934
|
}
|
|
768
935
|
if (op.action === "delete") {
|
|
@@ -784,7 +951,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
784
951
|
}
|
|
785
952
|
const removedSupport = await library.removeSupportFile(name, op.file_path ?? "", origin, anchor);
|
|
786
953
|
if (removedSupport.stale === true) return staleRefusal(removedSupport, name, op.file_path);
|
|
787
|
-
if (removedSupport.ok) await ctx.get("skillUsage")?.record?.(name, "patch").catch(() => {});
|
|
954
|
+
if (removedSupport.ok && removedSupport.noop !== true) await ctx.get("skillUsage")?.record?.(name, "patch").catch(() => {});
|
|
788
955
|
return removedSupport;
|
|
789
956
|
}
|
|
790
957
|
if (op.action === "restructure") {
|
|
@@ -801,14 +968,7 @@ function apply(ctx, rawConfig = {}) {
|
|
|
801
968
|
}
|
|
802
969
|
}
|
|
803
970
|
ctx.effect(() => () => {
|
|
804
|
-
|
|
805
|
-
cumulativeToolCalls.clear();
|
|
806
|
-
completionInjected.clear();
|
|
807
|
-
pendingCadenceReviews.clear();
|
|
808
|
-
pendingCadenceWarned.clear();
|
|
809
|
-
skipNextCadenceFire.clear();
|
|
810
|
-
cadenceResetWarned.clear();
|
|
811
|
-
reviewStateLocks.clear();
|
|
971
|
+
sessionState.dispose();
|
|
812
972
|
}, "dsh-evolution-review.cleanup");
|
|
813
973
|
}
|
|
814
974
|
/** Completion-channel decision: task finished normally AND the session is proven long. */
|
|
@@ -972,11 +1132,27 @@ function renderToolResultLine(data) {
|
|
|
972
1132
|
const output = resultBlocks.map((block) => Array.isArray(block.content) ? block.content.map((inner) => inner.type === "text" && typeof inner.text === "string" ? inner.text : "").join(" ") : typeof block.text === "string" ? block.text : "").join(" ").trim();
|
|
973
1133
|
return `[result]${shape?.error || resultBlocks.some((block) => block.isError === true) ? " [ERROR]" : ""} ${output.slice(0, 500)}`;
|
|
974
1134
|
}
|
|
1135
|
+
/**
|
|
1136
|
+
* PLAN S4.1 (2026-09-16, audit P2-12): text of one persisted content block,
|
|
1137
|
+
* or `''` for any other shape. Content blocks cross the durable session-log
|
|
1138
|
+
* boundary, so their runtime shape is `unknown` even where the static type
|
|
1139
|
+
* promises `{ type, text }` — a persisted `content: [null]` (the A2-7 shape)
|
|
1140
|
+
* used to TypeError in buildReviewRequest and the caller's catch dropped the
|
|
1141
|
+
* whole subagent review leg. This mirrors evolution-core signals.ts's private
|
|
1142
|
+
* `textOfBlock` (same guard, same rationale); it is not imported because core
|
|
1143
|
+
* keeps that helper module-private, and this file's other block renderer
|
|
1144
|
+
* (renderToolResultLine) guards its own inner-block shapes inline.
|
|
1145
|
+
*/
|
|
1146
|
+
function textOfPersistedBlock(block) {
|
|
1147
|
+
if (block === null || typeof block !== "object") return "";
|
|
1148
|
+
const candidate = block;
|
|
1149
|
+
return candidate.type === "text" && typeof candidate.text === "string" ? candidate.text : "";
|
|
1150
|
+
}
|
|
975
1151
|
function buildReviewRequest(session, kind, signal, maxMessages, maxMessageChars) {
|
|
976
1152
|
const messages = [];
|
|
977
1153
|
const surface = session.deriveMessages();
|
|
978
1154
|
for (const message of surface.slice(-maxMessages)) if (message.role === "user" || message.role === "assistant") {
|
|
979
|
-
const text = message.content.map(
|
|
1155
|
+
const text = message.content.map(textOfPersistedBlock).join(" ").trim();
|
|
980
1156
|
if (text) messages.push(`${message.role.toUpperCase()}: ${text.slice(0, maxMessageChars)}`);
|
|
981
1157
|
}
|
|
982
1158
|
const toolLines = [];
|
|
@@ -1009,4 +1185,4 @@ function buildReviewRequest(session, kind, signal, maxMessages, maxMessageChars)
|
|
|
1009
1185
|
].join("\n");
|
|
1010
1186
|
}
|
|
1011
1187
|
//#endregion
|
|
1012
|
-
export { Config, REVIEW_OUTPUT_SCHEMA, apply, clampReviewConfig, filterUnreadSkillOps, inject, name, renderToolResultLine, shouldCompletionReview, sweepDeadSessionEntries };
|
|
1188
|
+
export { Config, REVIEW_OUTPUT_SCHEMA, apply, buildReviewRequest, clampReviewConfig, filterUnreadSkillOps, inject, name, renderToolResultLine, shouldCompletionReview, sweepDeadSessionEntries };
|
package/lib/types/index.d.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { Context } from '@deepseek-ai/cordis';
|
|
6
6
|
import z from '@deepseek-ai/schemastery';
|
|
7
|
+
import type { Session } from '@deepseek-ai/dsh-session';
|
|
8
|
+
import { type ReviewKind } from '@lmzhen/dsh-evolution-core';
|
|
7
9
|
export declare const name = "evolution-review";
|
|
8
10
|
export declare const inject: string[];
|
|
9
11
|
export interface Config {
|
|
@@ -162,5 +164,10 @@ export declare function filterUnreadSkillOps(ops: Array<{
|
|
|
162
164
|
* buildReviewRequest and is unchanged).
|
|
163
165
|
*/
|
|
164
166
|
export declare function renderToolResultLine(data: unknown): string;
|
|
167
|
+
export declare function buildReviewRequest(session: Session, kind: ReviewKind, signal: {
|
|
168
|
+
toolCalls: number;
|
|
169
|
+
userChars: number;
|
|
170
|
+
assistantChars: number;
|
|
171
|
+
}, maxMessages: number, maxMessageChars: number): string;
|
|
165
172
|
export {};
|
|
166
173
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A registry of one plugin instance's session-keyed collections.
|
|
3
|
+
*
|
|
4
|
+
* P1-9 (S2-2): the review plugin kept eight per-session maps and cleared them
|
|
5
|
+
* from a hand-written list inside its dispose hook. The list drifted — the
|
|
6
|
+
* 0.3.38-0.3.42 additions (pendingCadenceReviews, pendingCadenceWarned,
|
|
7
|
+
* skipNextCadenceFire, cadenceResetWarned) were missing from it until V7-16 —
|
|
8
|
+
* because nothing tied a declaration to its cleanup. Registering at the
|
|
9
|
+
* declaration site removes the list: a collection clears because it was
|
|
10
|
+
* registered, and tests/session-state-ownership.spec.ts fails when a new
|
|
11
|
+
* `new Map<SessionId, …>` skips the registry.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately package-local: evolution-review is the only current consumer
|
|
14
|
+
* (commands holds no per-session state, and state-json's warn dedupe sets are
|
|
15
|
+
* process-lifetime by design). Promote it to evolution-core when a second
|
|
16
|
+
* package needs the same registration discipline.
|
|
17
|
+
*/
|
|
18
|
+
export declare class SessionScopedState {
|
|
19
|
+
private readonly registered;
|
|
20
|
+
/**
|
|
21
|
+
* Register a collection and return it, so declaration and registration stay
|
|
22
|
+
* one statement.
|
|
23
|
+
*
|
|
24
|
+
* @param key - the declaring variable name; also the diagnostic name.
|
|
25
|
+
* @param collection - any collection exposing clear().
|
|
26
|
+
* @returns the same collection.
|
|
27
|
+
*/
|
|
28
|
+
add<T extends {
|
|
29
|
+
clear(): void;
|
|
30
|
+
}>(key: string, collection: T): T;
|
|
31
|
+
/** Registered keys in registration order (diagnostics and the ownership spec). */
|
|
32
|
+
keys(): string[];
|
|
33
|
+
/** Clear every registered collection. Idempotent; safe on a partially used mount. */
|
|
34
|
+
dispose(): void;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=session-state.d.ts.map
|
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.83",
|
|
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.3.
|
|
31
|
-
"@lmzhen/dsh-evolution-core": "^0.3.
|
|
32
|
-
"@lmzhen/dsh-evolution-plan-validator": "^0.3.
|
|
30
|
+
"@lmzhen/dsh-evolution-approval": "^0.3.83",
|
|
31
|
+
"@lmzhen/dsh-evolution-core": "^0.3.83",
|
|
32
|
+
"@lmzhen/dsh-evolution-plan-validator": "^0.3.83"
|
|
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.3.
|
|
41
|
-
"@lmzhen/dsh-evolution-policy": "^0.3.
|
|
40
|
+
"@lmzhen/dsh-evolution-state": "^0.3.83",
|
|
41
|
+
"@lmzhen/dsh-evolution-policy": "^0.3.83"
|
|
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.3.
|
|
52
|
-
"@lmzhen/dsh-evolution-core": "^0.3.
|
|
53
|
-
"@lmzhen/dsh-evolution-curator": "^0.3.
|
|
54
|
-
"@lmzhen/dsh-evolution-plan-validator": "^0.3.
|
|
55
|
-
"@lmzhen/dsh-evolution-state": "^0.3.
|
|
51
|
+
"@lmzhen/dsh-evolution-approval": "^0.3.83",
|
|
52
|
+
"@lmzhen/dsh-evolution-core": "^0.3.83",
|
|
53
|
+
"@lmzhen/dsh-evolution-curator": "^0.3.83",
|
|
54
|
+
"@lmzhen/dsh-evolution-plan-validator": "^0.3.83",
|
|
55
|
+
"@lmzhen/dsh-evolution-state": "^0.3.83"
|
|
56
56
|
}
|
|
57
57
|
}
|