dsh-continual-evolve 0.11.0 → 0.12.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.
Files changed (154) hide show
  1. package/README.md +35 -115
  2. package/README.zh.md +26 -93
  3. package/cordis.patch.yml +2 -2
  4. package/lib/index.d.ts +44 -225
  5. package/lib/index.js +30 -239
  6. package/lib/memory-guide.d.ts +13 -14
  7. package/lib/memory-guide.js +32 -21
  8. package/lib/memory-section.d.ts +77 -0
  9. package/lib/memory-section.js +189 -0
  10. package/package.json +2 -17
  11. package/lib/apply.d.ts +0 -22
  12. package/lib/apply.js +0 -157
  13. package/lib/approval.d.ts +0 -73
  14. package/lib/approval.js +0 -127
  15. package/lib/audit-log.d.ts +0 -19
  16. package/lib/audit-log.js +0 -52
  17. package/lib/auto.d.ts +0 -236
  18. package/lib/auto.js +0 -773
  19. package/lib/autocase.d.ts +0 -38
  20. package/lib/autocase.js +0 -76
  21. package/lib/benchmark-command.d.ts +0 -9
  22. package/lib/benchmark-command.js +0 -357
  23. package/lib/benchmark.d.ts +0 -151
  24. package/lib/benchmark.js +0 -250
  25. package/lib/command-util.d.ts +0 -70
  26. package/lib/command-util.js +0 -123
  27. package/lib/command.d.ts +0 -13
  28. package/lib/command.js +0 -134
  29. package/lib/consolidate.d.ts +0 -90
  30. package/lib/consolidate.js +0 -180
  31. package/lib/copy.d.ts +0 -37
  32. package/lib/copy.js +0 -58
  33. package/lib/declines.d.ts +0 -103
  34. package/lib/declines.js +0 -179
  35. package/lib/evaluate.d.ts +0 -74
  36. package/lib/evaluate.js +0 -271
  37. package/lib/evolve-event.d.ts +0 -40
  38. package/lib/evolve-event.js +0 -60
  39. package/lib/extraction-phase.d.ts +0 -132
  40. package/lib/extraction-phase.js +0 -236
  41. package/lib/failures.d.ts +0 -42
  42. package/lib/failures.js +0 -171
  43. package/lib/fate.d.ts +0 -130
  44. package/lib/fate.js +0 -350
  45. package/lib/goal-command.d.ts +0 -7
  46. package/lib/goal-command.js +0 -37
  47. package/lib/goal.d.ts +0 -72
  48. package/lib/goal.js +0 -72
  49. package/lib/inject.d.ts +0 -90
  50. package/lib/inject.js +0 -164
  51. package/lib/injection-caps.d.ts +0 -4
  52. package/lib/injection-caps.js +0 -4
  53. package/lib/injection-directory.d.ts +0 -29
  54. package/lib/injection-directory.js +0 -78
  55. package/lib/injection-rank.d.ts +0 -85
  56. package/lib/injection-rank.js +0 -161
  57. package/lib/listener.d.ts +0 -45
  58. package/lib/listener.js +0 -199
  59. package/lib/llm-text.d.ts +0 -104
  60. package/lib/llm-text.js +0 -140
  61. package/lib/logfile.d.ts +0 -71
  62. package/lib/logfile.js +0 -159
  63. package/lib/maintenance-commands.d.ts +0 -23
  64. package/lib/maintenance-commands.js +0 -136
  65. package/lib/memory-agent.d.ts +0 -93
  66. package/lib/memory-agent.js +0 -300
  67. package/lib/memory-apply.d.ts +0 -44
  68. package/lib/memory-apply.js +0 -202
  69. package/lib/memory-benchmark.d.ts +0 -76
  70. package/lib/memory-benchmark.js +0 -170
  71. package/lib/memory-index.d.ts +0 -114
  72. package/lib/memory-index.js +0 -197
  73. package/lib/memory-manifest.d.ts +0 -16
  74. package/lib/memory-manifest.js +0 -70
  75. package/lib/message-source.d.ts +0 -23
  76. package/lib/message-source.js +0 -12
  77. package/lib/mount-command.d.ts +0 -10
  78. package/lib/mount-command.js +0 -48
  79. package/lib/mount.d.ts +0 -42
  80. package/lib/mount.js +0 -212
  81. package/lib/notify.d.ts +0 -48
  82. package/lib/notify.js +0 -57
  83. package/lib/plan-command.d.ts +0 -12
  84. package/lib/plan-command.js +0 -38
  85. package/lib/plan.d.ts +0 -16
  86. package/lib/plan.js +0 -126
  87. package/lib/planner.d.ts +0 -61
  88. package/lib/planner.js +0 -144
  89. package/lib/pool.d.ts +0 -7
  90. package/lib/pool.js +0 -25
  91. package/lib/prefix-cache.d.ts +0 -79
  92. package/lib/prefix-cache.js +0 -168
  93. package/lib/project.d.ts +0 -28
  94. package/lib/project.js +0 -66
  95. package/lib/projection.d.ts +0 -22
  96. package/lib/projection.js +0 -112
  97. package/lib/promotion.d.ts +0 -94
  98. package/lib/promotion.js +0 -171
  99. package/lib/recall.d.ts +0 -85
  100. package/lib/recall.js +0 -142
  101. package/lib/record-language.d.ts +0 -85
  102. package/lib/record-language.js +0 -152
  103. package/lib/render.d.ts +0 -13
  104. package/lib/render.js +0 -80
  105. package/lib/review-scheduler.d.ts +0 -38
  106. package/lib/review-scheduler.js +0 -183
  107. package/lib/review.d.ts +0 -60
  108. package/lib/review.js +0 -145
  109. package/lib/rollback.d.ts +0 -9
  110. package/lib/rollback.js +0 -68
  111. package/lib/rubric.d.ts +0 -29
  112. package/lib/rubric.js +0 -119
  113. package/lib/runtime-commands.d.ts +0 -14
  114. package/lib/runtime-commands.js +0 -115
  115. package/lib/runtime.d.ts +0 -24
  116. package/lib/runtime.js +0 -87
  117. package/lib/score.d.ts +0 -64
  118. package/lib/score.js +0 -191
  119. package/lib/search.d.ts +0 -83
  120. package/lib/search.js +0 -136
  121. package/lib/service.d.ts +0 -63
  122. package/lib/service.js +0 -253
  123. package/lib/skill-render.d.ts +0 -23
  124. package/lib/skill-render.js +0 -69
  125. package/lib/skill.d.ts +0 -15
  126. package/lib/skill.js +0 -80
  127. package/lib/skillquality.d.ts +0 -85
  128. package/lib/skillquality.js +0 -320
  129. package/lib/source.d.ts +0 -30
  130. package/lib/source.js +0 -43
  131. package/lib/state.d.ts +0 -26
  132. package/lib/state.js +0 -151
  133. package/lib/store.d.ts +0 -61
  134. package/lib/store.js +0 -164
  135. package/lib/token-usage.d.ts +0 -68
  136. package/lib/token-usage.js +0 -211
  137. package/lib/tool.d.ts +0 -34
  138. package/lib/tool.js +0 -320
  139. package/lib/turn-snapshot.d.ts +0 -78
  140. package/lib/turn-snapshot.js +0 -252
  141. package/lib/types.d.ts +0 -218
  142. package/lib/types.js +0 -133
  143. package/lib/usage.d.ts +0 -36
  144. package/lib/usage.js +0 -98
  145. package/lib/validate.d.ts +0 -28
  146. package/lib/validate.js +0 -181
  147. package/lib/wrapup-candidates.d.ts +0 -127
  148. package/lib/wrapup-candidates.js +0 -161
  149. package/lib/wrapup-command.d.ts +0 -10
  150. package/lib/wrapup-command.js +0 -243
  151. package/lib/wrapup-proposals.d.ts +0 -101
  152. package/lib/wrapup-proposals.js +0 -281
  153. package/lib/wrapup.d.ts +0 -56
  154. package/lib/wrapup.js +0 -105
package/lib/auto.js DELETED
@@ -1,773 +0,0 @@
1
- /**
2
- * The automatic evolution driver: watches agent turns and session compaction.
3
- * In the production wiring it runs only the dedicated Memory Agent; the
4
- * general review/planner and local-fate phases remain available to direct
5
- * callers but are not entered by memory-only mode. All auxiliary work is
6
- * fire-and-forget with error containment: an automatic failure never disturbs
7
- * the agent loop.
8
- *
9
- * Every gate decision (approved / declined / failed / skipped) is appended to
10
- * `<dshHome>/evolve/reviews.jsonl` so auto-review activity is durably
11
- * auditable — the server console is not a reliable place to look.
12
- *
13
- * Hook wiring:
14
- * - `agent/turn-stopping` records the successful turn boundary.
15
- * - `agent/status` (idle) captures an incremental snapshot and feeds the
16
- * per-session latest-pending serial scheduler.
17
- * - `session/event` (compaction/start) forces a snapshot flush before data is
18
- * summarized away.
19
- */
20
- import { appendFileSync, mkdirSync } from "node:fs";
21
- import { join } from "node:path";
22
- import { slug } from "./types.js";
23
- import { planWithLlm } from "./planner.js";
24
- import { reviewAutoRefine } from "./review.js";
25
- import { goalServiceOf } from "./goal.js";
26
- import { notifyAutoReview, notifyMemoryExtraction } from "./notify.js";
27
- import { runLocalFatePhase } from "./fate.js";
28
- import { mergeHarnessStates } from "./state.js";
29
- import { projectKeyOf } from "./project.js";
30
- import { questionServiceOf } from "./approval.js";
31
- import { buildEvolveCompleteEvent, emitEvolveComplete } from "./evolve-event.js";
32
- import { DEFAULT_REVIEWS_RETAIN, pruneJsonlFile } from "./store.js";
33
- import { isGateEnabled } from "./runtime.js";
34
- import { compareReviewCursors, createReviewScheduler } from "./review-scheduler.js";
35
- import { captureTurnSnapshot, sliceTurnSnapshot } from "./turn-snapshot.js";
36
- import { captureAutoCase } from "./autocase.js";
37
- import { loadDeclinedMemory, matchDeclinedCheckpoint } from "./declines.js";
38
- import { tokenize } from "./search.js";
39
- import { applyMemoryExtractionProposal, buildMemoryManifest, runMemoryAgent, } from "./memory-agent.js";
40
- import { asLlmSessionId } from "./llm-text.js";
41
- import { resolveRecordLanguage } from "./record-language.js";
42
- import { resolveDialogLanguage, skillConsultCopy, skillEditLine } from "./copy.js";
43
- /** Turns a rejected skill candidate stays silent before being offered again. */
44
- export const SKILL_CONSULT_COOLDOWN_TURNS = 10;
45
- /** Default session-close drain budget: an in-flight extraction gets 15s to settle. */
46
- export const SESSION_CLOSE_DRAIN_MS_DEFAULT = 15000;
47
- /**
48
- * Resolve the session-close drain budget: absent/invalid → default, 0 →
49
- * immediate abort (legacy). Negative or non-finite values fail back to the
50
- * default rather than silently disabling the drain.
51
- */
52
- export function resolveSessionCloseDrainMs(raw) {
53
- if (raw === undefined)
54
- return SESSION_CLOSE_DRAIN_MS_DEFAULT;
55
- if (typeof raw !== "number" || !Number.isFinite(raw) || raw < 0)
56
- return SESSION_CLOSE_DRAIN_MS_DEFAULT;
57
- return Math.floor(raw);
58
- }
59
- /**
60
- * Bounded session-close drain for one scheduler: wait for in-flight work up
61
- * to drainMs, then abort the owner signal and drop pending captures. Never
62
- * rejects — disposal must not hang the host on a stuck model call, and the
63
- * abort below is idempotent when the drain already settled.
64
- */
65
- export function drainSchedulerOnDispose(scheduler, drainMs) {
66
- if (drainMs <= 0) {
67
- try {
68
- scheduler.shutdown();
69
- }
70
- catch {
71
- // shutdown is flag-setting plus abort: nothing observable to do
72
- }
73
- return Promise.resolve();
74
- }
75
- let timer;
76
- const timeout = new Promise((resolve) => {
77
- timer = setTimeout(resolve, drainMs);
78
- });
79
- const settle = () => {
80
- if (timer !== undefined)
81
- clearTimeout(timer);
82
- try {
83
- scheduler.shutdown();
84
- }
85
- catch {
86
- // same as above — the abort is best-effort by design
87
- }
88
- };
89
- return Promise.race([scheduler.drain(), timeout]).then(settle, settle);
90
- }
91
- export function registerAutoReview(ctx, engine, config) {
92
- const perSession = new Map();
93
- const schedulers = new Map();
94
- const captureTails = new Map();
95
- const disposedSessions = new Set();
96
- const logger = ctx.logger("continual-evolve");
97
- const reviewsPath = join(engine.baseDir, "evolve", "reviews.jsonl");
98
- const defaultEnabled = config.enabledByDefault ?? true;
99
- const record = (entry) => {
100
- try {
101
- mkdirSync(join(engine.baseDir, "evolve"), { recursive: true });
102
- appendFileSync(reviewsPath, `${JSON.stringify({ ...entry, timestamp: new Date().toISOString() })}\n`, "utf8");
103
- }
104
- catch (cause) {
105
- logger.warn(`failed to record auto-review: ${cause instanceof Error ? cause.message : String(cause)}`);
106
- return;
107
- }
108
- // Storage hygiene (#20): bound the audit trail at write time.
109
- try {
110
- pruneJsonlFile(reviewsPath, config.reviewsRetain ?? DEFAULT_REVIEWS_RETAIN);
111
- }
112
- catch {
113
- // ignored — the next record retries
114
- }
115
- };
116
- const schedulerFor = (agent, state) => {
117
- const existing = schedulers.get(agent.id);
118
- if (existing)
119
- return existing;
120
- const scheduler = createReviewScheduler(async ({ snapshot, signal }) => {
121
- if (!isGateEnabled(engine.baseDir, defaultEnabled))
122
- return "aborted";
123
- if (!snapshot.eligible) {
124
- record({
125
- sessionId: snapshot.sessionId,
126
- reason: snapshot.reason,
127
- turnsSinceLastReview: state.turns - state.lastReviewAt,
128
- outcome: "skipped",
129
- rationale: `snapshot ${snapshot.cursor} skipped: ${snapshot.skipReason ?? "not eligible"}`,
130
- });
131
- return "no-op";
132
- }
133
- try {
134
- if (config.memoryOnly) {
135
- await runMemoryExtractionPhase(ctx, engine, agent, config, state, snapshot, record, signal);
136
- }
137
- else {
138
- await runGate(ctx, engine, agent, config, state, snapshot, record, signal);
139
- }
140
- return "success";
141
- }
142
- catch (cause) {
143
- const message = cause instanceof Error ? cause.message : String(cause);
144
- logger.warn(`auto-review failed for ${agent.id}: ${message}`);
145
- record({
146
- sessionId: agent.id,
147
- reason: snapshot.reason,
148
- turnsSinceLastReview: state.turns - state.lastReviewAt,
149
- outcome: "failed",
150
- rationale: `gate error: ${message}`,
151
- });
152
- return "error";
153
- }
154
- }, (snapshot) => snapshot.cursor);
155
- schedulers.set(agent.id, scheduler);
156
- return scheduler;
157
- };
158
- const captureAndSchedule = (agent, state, reason) => {
159
- if (disposedSessions.has(agent.id) || !isGateEnabled(engine.baseDir, defaultEnabled))
160
- return;
161
- const turn = state.completedTurn;
162
- const previousSnapshotTurn = state.lastSnapshotTurn;
163
- if (reason === "turn_snapshot" && turn <= previousSnapshotTurn)
164
- return;
165
- if (reason === "turn_snapshot")
166
- state.lastSnapshotTurn = turn;
167
- const previousCapture = captureTails.get(agent.id) ?? Promise.resolve();
168
- const captureTask = previousCapture.then(async () => {
169
- if (disposedSessions.has(agent.id) || !isGateEnabled(engine.baseDir, defaultEnabled))
170
- return;
171
- const cursor = schedulerFor(agent, state).getCursor();
172
- const snapshot = await captureTurnSnapshot(ctx, agent, {
173
- turn,
174
- reason,
175
- ...(cursor !== undefined ? { cursor } : {}),
176
- maxChars: config.maxInputChars,
177
- ...(config.memoryMinUserWords !== undefined ? { minUserWords: config.memoryMinUserWords } : {}),
178
- });
179
- if (!disposedSessions.has(agent.id))
180
- schedulerFor(agent, state).schedule(snapshot);
181
- }).catch((cause) => {
182
- const message = cause instanceof Error ? cause.message : String(cause);
183
- logger.warn(`auto-review snapshot failed for ${agent.id}: ${message}`);
184
- record({
185
- sessionId: agent.id,
186
- reason,
187
- turnsSinceLastReview: state.turns - state.lastReviewAt,
188
- outcome: "failed",
189
- rationale: `snapshot error: ${message}`,
190
- });
191
- if (reason === "turn_snapshot" && state.lastSnapshotTurn === turn) {
192
- state.lastSnapshotTurn = previousSnapshotTurn;
193
- }
194
- });
195
- captureTails.set(agent.id, captureTask);
196
- };
197
- // DSH emits this awaited boundary when a turn is about to close. The idle
198
- // transition is the safe point at which to read the durable surface.
199
- ctx.on("agent/turn-stopping", (payload) => {
200
- const agent = payload.agent;
201
- if (!agent) {
202
- logger.warn(`auto-review gate: agent/turn-stopping payload missing agent; skipping count`);
203
- return;
204
- }
205
- const state = stateFor(perSession, agent.id);
206
- state.turns += 1;
207
- if (typeof payload.turn === "number")
208
- state.completedTurn = Math.max(state.completedTurn, payload.turn);
209
- });
210
- ctx.on("agent/status", (payload) => {
211
- const agent = payload.agent;
212
- if (!agent || payload.status !== "idle")
213
- return;
214
- const state = stateFor(perSession, agent.id);
215
- if (state.completedTurn <= state.lastSnapshotTurn)
216
- return;
217
- captureAndSchedule(agent, state, "turn_snapshot");
218
- });
219
- // Diagnostic: the armed marker proves the listener was registered even when
220
- // its runtime default is off, distinguishing "not registered" from "off".
221
- try {
222
- mkdirSync(join(engine.baseDir, "evolve"), { recursive: true });
223
- appendFileSync(reviewsPath, `${JSON.stringify({
224
- timestamp: new Date().toISOString(),
225
- sessionId: "boot",
226
- reason: "boot",
227
- turnsSinceLastReview: 0,
228
- outcome: "armed",
229
- rationale: `automatic evolution listener registered (mode=${config.memoryOnly ? "memory-only" : "full"}; default=${defaultEnabled ? "on" : "off"}; per-success-turn snapshots)`,
230
- })}\n`, "utf8");
231
- }
232
- catch (cause) {
233
- logger.warn(`failed to write armed marker: ${cause instanceof Error ? cause.message : String(cause)}`);
234
- }
235
- try {
236
- pruneJsonlFile(reviewsPath, config.reviewsRetain ?? DEFAULT_REVIEWS_RETAIN);
237
- }
238
- catch {
239
- // ignored — the next record retries
240
- }
241
- ctx.on("agent/disposed", (payload) => {
242
- const agent = payload.agent;
243
- if (!agent)
244
- return;
245
- disposedSessions.add(agent.id);
246
- const scheduler = schedulers.get(agent.id);
247
- schedulers.delete(agent.id);
248
- captureTails.delete(agent.id);
249
- perSession.delete(agent.id);
250
- if (scheduler) {
251
- // Bounded drain (P1): an in-flight extraction gets drainMs to
252
- // settle so its result is not lost on session close; afterwards
253
- // the owner signal aborts. Fire-and-forget — disposal must never
254
- // hang the host, and the helper never rejects.
255
- void drainSchedulerOnDispose(scheduler, resolveSessionCloseDrainMs(config.sessionCloseDrainMs));
256
- }
257
- });
258
- ctx.on("session/event", (session, event) => {
259
- // ZCode schedules extraction from successful turns, not from compaction.
260
- // Keep the legacy compaction path for direct/full callers, but do not
261
- // add an extra automatic Memory Agent trigger in memory-only mode.
262
- if (config.memoryOnly)
263
- return;
264
- if (event.type !== "compaction/start")
265
- return;
266
- const agents = ctx.agents;
267
- const agent = agents?.get(session.id);
268
- if (!agent)
269
- return; // no live agent for that session (e.g. cold read)
270
- const state = stateFor(perSession, agent.id);
271
- captureAndSchedule(agent, state, "compact");
272
- });
273
- }
274
- /**
275
- * Gap C1: parse a "provider/model" or "model" string into its components.
276
- * Returns undefined when the input is empty (no override). A bare model name
277
- * falls back to the agent's provider, then "deepseek".
278
- * Exported for unit testing (the advanceGateState precedent); production
279
- * resolves it inside runReviewPhase.
280
- */
281
- export function parseReviewModel(reviewModel, fallbackProvider) {
282
- if (!reviewModel || reviewModel.trim().length === 0)
283
- return undefined;
284
- const slash = reviewModel.indexOf("/");
285
- if (slash > 0) {
286
- return { provider: reviewModel.slice(0, slash), model: reviewModel.slice(slash + 1) };
287
- }
288
- return { provider: fallbackProvider ?? "deepseek", model: reviewModel };
289
- }
290
- function stateFor(map, sessionId) {
291
- let state = map.get(sessionId);
292
- if (!state) {
293
- state = {
294
- turns: 0,
295
- completedTurn: 0,
296
- lastSnapshotTurn: 0,
297
- memoryDecisions: {},
298
- lastReviewAt: 0,
299
- running: false,
300
- skillRejects: new Map(),
301
- lastFateAt: 0,
302
- fateRejects: new Map(),
303
- goalBlockStreak: 0,
304
- };
305
- map.set(sessionId, state);
306
- }
307
- return state;
308
- }
309
- function advanceMemoryCheckpoint(state, next) {
310
- if (state.memoryCheckpoint === undefined) {
311
- state.memoryCheckpoint = next;
312
- return;
313
- }
314
- const order = compareReviewCursors(next, state.memoryCheckpoint);
315
- if (order === undefined || order > 0)
316
- state.memoryCheckpoint = next;
317
- }
318
- function rememberMemoryDecision(state, key, decision) {
319
- state.memoryDecisions[key] = decision;
320
- const keys = Object.keys(state.memoryDecisions);
321
- while (keys.length > 128)
322
- delete state.memoryDecisions[keys.shift() ?? ""];
323
- }
324
- /**
325
- * The state view the gate and planner judge: the session's local entries
326
- * merged with the project and global stores, each entry carrying its real
327
- * scope. Without the global half the gate cannot see that a topic is already
328
- * covered cross-session and happily re-sediments a local duplicate of it.
329
- *
330
- * The merged view is read-only context — applying still targets the raw
331
- * local state (baseline checks compare local entries only).
332
- */
333
- export function loadGateHarnessView(engine, sessionId, opts) {
334
- const projectState = opts?.projectKey ? engine.load("project", opts.projectKey) : undefined;
335
- return mergeHarnessStates(engine.load("global", undefined), engine.load("local", sessionId), projectState ? { projectState } : undefined);
336
- }
337
- /**
338
- * One gate run = review phase + local-fate phase (#11 P2). The review phase
339
- * judges and applies local refinements; the local-fate phase then gives the
340
- * session's existing local entries a running exit (promote/archive proposals,
341
- * consulted before they land). Running fate AFTER the review keeps the
342
- * review's baseline fresh — fate re-loads the store and never races the
343
- * review's optimistic-concurrency checks.
344
- */
345
- async function runGate(ctx, engine, agent, config, state, snapshot, record, signal) {
346
- // Reentry guard (review audit 2026-08-28 S3): a gate run holds LLM calls
347
- // and possibly a user question for a long time; an idle/compaction
348
- // trigger overlapping the run would start a second concurrent pipeline
349
- // whose stale whole-file saves clobber the first run's writes.
350
- if (state.running) {
351
- throw new Error(`previous gate run still in flight for ${agent.id}`);
352
- }
353
- state.running = true;
354
- try {
355
- await runMemoryExtractionPhase(ctx, engine, agent, config, state, snapshot, record, signal);
356
- await runReviewPhase(ctx, engine, agent, config, state, snapshot, record, signal);
357
- // D3: a goal stuck in "blocked" for consecutive gate runs gets one
358
- // local-fate assessment (the pipeline below), so whatever led the
359
- // goal astray is distilled before the session moves on.
360
- await runGoalBlockedFate(ctx, engine, agent, config, state, snapshot.reason, record);
361
- await runLocalFatePhase(ctx, engine, agent, config, state, snapshot.reason, record);
362
- }
363
- finally {
364
- state.running = false;
365
- }
366
- }
367
- /**
368
- * D3 (goal blocked → wrap-up coupling, reverse direction): count consecutive
369
- * gate runs whose goal is in phase "blocked"; when the streak reaches
370
- * `goalBlockedWrapupTurns`, run ONE local-fate assessment (same pipeline as
371
- * the normal fate dimension — audit, classify, consult, apply deterministically).
372
- * The streak resets on any non-blocked run and after a triggered assessment;
373
- * a declined proposal is then protected by the normal fate cooldown, so a
374
- * blocked session can never be nagged into another dialog.
375
- *
376
- * Exported for unit testing (the advanceGateState precedent); production runs
377
- * it from runGate.
378
- */
379
- export async function runGoalBlockedFate(ctx, engine, agent, config, state, _reason, record) {
380
- if (config.goalBlockedWrapupTurns <= 0)
381
- return;
382
- const goal = goalServiceOf(ctx)?.get(agent);
383
- if (goal?.phase !== "blocked") {
384
- state.goalBlockStreak = 0;
385
- return;
386
- }
387
- state.goalBlockStreak += 1;
388
- if (state.goalBlockStreak < config.goalBlockedWrapupTurns) {
389
- return;
390
- }
391
- state.goalBlockStreak = 0; // one assessment per streak; declines follow the fate cooldown
392
- const logger = ctx.logger("continual-evolve");
393
- logger.info(`auto-review goal-blocked trigger [${agent.id}]: ${config.goalBlockedWrapupTurns} consecutive blocked gate runs → local-fate assessment`);
394
- await runLocalFatePhase(ctx, engine, agent, config, state, "goal_blocked", record);
395
- }
396
- /**
397
- * Dedicated ZCode-style memory phase. It shares the scheduler snapshot and
398
- * snapshot with the general review, but owns its request loop, manifest and
399
- * memory-only tool policy. Its phase-specific checkpoint advances after a
400
- * successful/no-op memory outcome; later review/fate failures do not replay
401
- * old memory decisions, while memory failures leave the boundary retryable.
402
- */
403
- export async function runMemoryExtractionPhase(ctx, engine, agent, config, state, snapshot, record, signal) {
404
- const sessionId = agent.id;
405
- const turnsSinceLastReview = state.turns - state.lastReviewAt;
406
- const startedAt = Date.now();
407
- const memorySnapshot = state.memoryCheckpoint ? sliceTurnSnapshot(snapshot, state.memoryCheckpoint) : snapshot;
408
- if (!memorySnapshot.eligible || !memorySnapshot.trajectory) {
409
- advanceMemoryCheckpoint(state, snapshot.cursor);
410
- ctx.logger("continual-evolve").info(`memory agent no-op (${snapshot.reason}) [${agent.id}]: no new eligible evidence after checkpoint`);
411
- record({
412
- sessionId,
413
- reason: snapshot.reason,
414
- turnsSinceLastReview,
415
- outcome: "noop",
416
- rationale: `memory no-op (${snapshot.reason}): no new eligible evidence after checkpoint`,
417
- durationMs: Date.now() - startedAt,
418
- memoryTurns: 0,
419
- memorySearches: 0,
420
- appliedEdits: 0,
421
- });
422
- return;
423
- }
424
- const declinedHit = matchDeclinedCheckpoint(loadDeclinedMemory(engine.baseDir), tokenize(memorySnapshot.trajectory));
425
- if (declinedHit !== undefined) {
426
- advanceMemoryCheckpoint(state, memorySnapshot.cursor);
427
- ctx.logger("continual-evolve").info(`memory agent pre-suppressed (${snapshot.reason}) [${agent.id}]: checkpoint overlaps declined ${declinedHit.entry.scope}:${declinedHit.entry.fingerprint} (coverage ${(declinedHit.coverage * 100).toFixed(0)}%, ${declinedHit.hits} tokens) — agent not invoked`);
428
- record({
429
- sessionId,
430
- reason: snapshot.reason,
431
- turnsSinceLastReview,
432
- outcome: "noop",
433
- rationale: `memory pre-suppressed (${snapshot.reason}): checkpoint overlaps declined ${declinedHit.entry.scope}:${declinedHit.entry.fingerprint} — agent not invoked`,
434
- durationMs: Date.now() - startedAt,
435
- memoryTurns: 0,
436
- memorySearches: 0,
437
- appliedEdits: 0,
438
- });
439
- return;
440
- }
441
- const projectKey = snapshot.projectKey ?? projectKeyOf(agent);
442
- const baselines = {
443
- local: engine.load("local", sessionId),
444
- global: engine.load("global", undefined),
445
- ...(projectKey ? { project: engine.load("project", projectKey) } : {}),
446
- };
447
- const manifest = buildMemoryManifest(loadGateHarnessView(engine, sessionId, projectKey ? { projectKey } : undefined));
448
- const overrideRoute = parseReviewModel(config.reviewModel, agent.options.provider);
449
- const provider = overrideRoute?.provider ?? agent.options.provider;
450
- const model = overrideRoute?.model ?? agent.options.model;
451
- if (!provider || !model)
452
- throw new Error("evolve: no provider/model route for the memory extraction agent");
453
- const tokenUsage = {
454
- baseDir: engine.baseDir,
455
- sessionId,
456
- retain: engine.retention.tokenUsage,
457
- onError: (cause) => ctx
458
- .logger("continual-evolve")
459
- .warn(`token-usage ledger failed for ${sessionId}: ${cause instanceof Error ? cause.message : String(cause)}`),
460
- };
461
- const run = await runMemoryAgent(ctx, {
462
- provider,
463
- model,
464
- sessionId: asLlmSessionId(sessionId),
465
- manifest,
466
- trajectory: memorySnapshot.trajectory,
467
- trajectoryEvents: memorySnapshot.events,
468
- maxOutputTokens: config.budgetTokens,
469
- ...(signal ? { signal } : {}),
470
- tokenUsage,
471
- language: resolveRecordLanguage({ configured: config.recordLanguage, ctx, trajectoryText: memorySnapshot.trajectory }),
472
- ...((config.prefixCacheMode !== undefined || config.prefixMaxChars !== undefined
473
- ? {
474
- prefixCache: {
475
- ...(config.prefixCacheMode !== undefined ? { mode: config.prefixCacheMode } : {}),
476
- ...(config.prefixMaxChars !== undefined ? { maxChars: config.prefixMaxChars } : {}),
477
- },
478
- }
479
- : {})),
480
- });
481
- if (run.proposal.edits.length === 0) {
482
- advanceMemoryCheckpoint(state, memorySnapshot.cursor);
483
- ctx.logger("continual-evolve").info(`memory agent no-op (${snapshot.reason}) [${sessionId}] after ${run.turns} turn(s): ${run.proposal.rationale}`);
484
- record({
485
- sessionId,
486
- reason: snapshot.reason,
487
- turnsSinceLastReview,
488
- outcome: "noop",
489
- rationale: `memory no-op (${snapshot.reason}) after ${run.turns} turn(s): ${run.proposal.rationale}`,
490
- durationMs: Date.now() - startedAt,
491
- memoryTurns: run.turns,
492
- memorySearches: run.searches,
493
- appliedEdits: 0,
494
- });
495
- return;
496
- }
497
- const source = { sessionId, ...(memorySnapshot.sourceSeqs.length > 0 ? { seqs: [...memorySnapshot.sourceSeqs] } : {}) };
498
- const application = await applyMemoryExtractionProposal(ctx, engine, run.proposal, {
499
- agent,
500
- baselines,
501
- ...(projectKey ? { projectKey } : {}),
502
- requireApproval: config.requireGlobalApproval ?? true,
503
- source,
504
- decisionCursor: memorySnapshot.cursor,
505
- scopeDecisions: state.memoryDecisions,
506
- onScopeDecision: (key, decision) => rememberMemoryDecision(state, key, decision),
507
- ...(config.recordLanguage !== undefined ? { recordLanguage: config.recordLanguage } : {}),
508
- ...(signal ? { signal } : {}),
509
- });
510
- for (const result of application.results) {
511
- emitEvolveComplete(engine.baseDir, buildEvolveCompleteEvent(result, `memory_agent:${snapshot.reason}`, sessionId), config.reviewsRetain ?? DEFAULT_REVIEWS_RETAIN);
512
- }
513
- state.memoryCheckpoint = memorySnapshot.cursor;
514
- const applied = application.results.reduce((count, result) => count + result.appliedEdits.filter((edit) => edit.applied).length, 0);
515
- const declined = application.declinedScopes.length > 0 ? `; declined scopes=${application.declinedScopes.join(",")}` : "";
516
- const durationMs = Date.now() - startedAt;
517
- for (const result of application.results) {
518
- record({
519
- sessionId,
520
- reason: snapshot.reason,
521
- turnsSinceLastReview,
522
- outcome: "applied",
523
- rationale: `memory applied (${snapshot.reason}) after ${run.turns} turn(s): ${run.proposal.rationale}${declined}`,
524
- refinementId: result.id,
525
- durationMs,
526
- memoryTurns: run.turns,
527
- memorySearches: run.searches,
528
- appliedEdits: result.appliedEdits.filter((edit) => edit.applied).length,
529
- });
530
- }
531
- if (application.declinedScopes.length > 0) {
532
- record({
533
- sessionId,
534
- reason: snapshot.reason,
535
- turnsSinceLastReview,
536
- outcome: "declined",
537
- rationale: `memory scopes declined (${snapshot.reason}): ${application.declinedScopes.join(",")}`,
538
- durationMs,
539
- memoryTurns: run.turns,
540
- memorySearches: run.searches,
541
- appliedEdits: 0,
542
- });
543
- }
544
- ctx.logger("continual-evolve").info(`memory agent applied ${applied} edit(s) across ${application.results.length} scope batch(es) [${sessionId}] after ${run.turns} turn(s)${declined}: ${run.proposal.rationale}`);
545
- // Visibility (P1 unified receipt): only applied outcomes wake the agent
546
- // with a follow-up — and only on the turn path, never mid-compaction.
547
- // No-op and declined-only runs stay audit-only (reviews.jsonl).
548
- if (config.notifyOnAutoReview && snapshot.reason === "turn_snapshot" && applied > 0) {
549
- notifyMemoryExtraction(ctx, agent, {
550
- outcome: "applied",
551
- results: application.results,
552
- ...(application.declinedScopes.length > 0 ? { declinedScopes: [...application.declinedScopes] } : {}),
553
- turns: run.turns,
554
- searches: run.searches,
555
- durationMs,
556
- });
557
- }
558
- }
559
- async function runReviewPhase(ctx, engine, agent, config, state, snapshot, record, signal) {
560
- const sessionId = agent.id;
561
- const reason = snapshot.reason;
562
- const turnsSinceLastReview = state.turns - state.lastReviewAt;
563
- const logger = ctx.logger("continual-evolve");
564
- const tokenUsage = {
565
- baseDir: engine.baseDir,
566
- sessionId,
567
- retain: engine.retention.tokenUsage,
568
- onError: (cause) => logger.warn(`token-usage ledger failed for ${sessionId}: ${cause instanceof Error ? cause.message : String(cause)}`),
569
- };
570
- const trajectory = snapshot.trajectory;
571
- if (!trajectory) {
572
- throw new Error("eligible snapshot has no trajectory");
573
- }
574
- // The gate judges the merged view (global + local, scopes labeled) so it
575
- // can recognize topics already covered globally and decline duplicates;
576
- // applying still targets the raw local store.
577
- const localState = engine.load("local", sessionId);
578
- const projectKey = projectKeyOf(agent);
579
- const harnessState = loadGateHarnessView(engine, sessionId, projectKey ? { projectKey } : undefined);
580
- const history = engine.history("local", sessionId);
581
- // Gap C1: resolve optional review model override.
582
- const reviewRoute = parseReviewModel(config.reviewModel, agent.options.provider);
583
- // Records follow the client language: explicit config first, else the
584
- // durable DSH preference, else this run's own trajectory.
585
- const recordLanguage = resolveRecordLanguage({ configured: config.recordLanguage, ctx, trajectoryText: trajectory });
586
- const review = await reviewAutoRefine(ctx, {
587
- agent,
588
- state: harnessState,
589
- history,
590
- trajectory,
591
- trajectoryEvents: snapshot.events,
592
- ...(signal ? { signal } : {}),
593
- context: { reason, turnsSinceLastReview },
594
- budgetTokens: config.budgetTokens,
595
- tokenUsage,
596
- language: recordLanguage,
597
- ...(reviewRoute ? { overrideProvider: reviewRoute.provider, overrideModel: reviewRoute.model } : {}),
598
- ...((config.prefixCacheMode !== undefined || config.prefixMaxChars !== undefined
599
- ? {
600
- prefixCache: {
601
- ...(config.prefixCacheMode !== undefined ? { mode: config.prefixCacheMode } : {}),
602
- ...(config.prefixMaxChars !== undefined ? { maxChars: config.prefixMaxChars } : {}),
603
- },
604
- }
605
- : {})),
606
- });
607
- state.lastReviewAt = state.turns;
608
- if (!review.shouldRefine) {
609
- logger.info(`auto-review declined (${reason}) [${sessionId}] after ${turnsSinceLastReview} turns: ${review.rationale}`);
610
- record({ sessionId, reason, turnsSinceLastReview, outcome: "declined", rationale: review.rationale });
611
- return;
612
- }
613
- const plannedProposal = await planWithLlm(ctx, {
614
- agent,
615
- state: harnessState,
616
- history,
617
- trajectory: snapshot.trajectory,
618
- trajectoryEvents: snapshot.events,
619
- ...(signal ? { signal } : {}),
620
- ...(review.instructions ? { instructions: review.instructions } : {}),
621
- global: false,
622
- // Read the skill-creator template facts (fallback: builtin distilled
623
- // guide) so skill proposals follow the standard.
624
- skillsRoot: join(engine.baseDir, "skills"),
625
- tokenUsage,
626
- language: recordLanguage,
627
- ...((config.prefixCacheMode !== undefined || config.prefixMaxChars !== undefined
628
- ? {
629
- prefixCache: {
630
- ...(config.prefixCacheMode !== undefined ? { mode: config.prefixCacheMode } : {}),
631
- ...(config.prefixMaxChars !== undefined ? { maxChars: config.prefixMaxChars } : {}),
632
- },
633
- }
634
- : {})),
635
- });
636
- const proposal = stripDedicatedMemoryEdits(plannedProposal);
637
- const memoryOnlyPlan = isDedicatedMemoryOnlyProposal(plannedProposal);
638
- // Skills are governed resources: an auto-created skill is OFFERED to the
639
- // user for a decision (固化/不固化) before it lands — the gate never
640
- // writes a skill silently. Without consent the skill edits are withheld
641
- // and the rest of the proposal proceeds as usual.
642
- const { skillEdits, otherEdits } = splitSkillEdits(proposal);
643
- const skillConsented = await consultSkillEdits(ctx, agent, skillEdits, state, recordLanguage);
644
- const finalProposal = skillConsented
645
- ? proposal
646
- : {
647
- ...proposal,
648
- edits: otherEdits,
649
- summary: skillEdits.length > 0 ? `${proposal.summary} (skill edits withheld — pending user decision)` : proposal.summary,
650
- };
651
- if (finalProposal.edits.length === 0) {
652
- const withheld = skillEdits.length > 0 ? " (skill proposal withheld — user not consulted or declined)" : "";
653
- logger.info(`auto-review declined (${reason}) [${sessionId}]: no consented edits${withheld} — ${review.rationale}`);
654
- // P1 auto-case capture: an attempted evolution that never landed is a
655
- // regression asset. Contained — capture failure must not disturb the gate.
656
- if (config.autoCase && !memoryOnlyPlan) {
657
- try {
658
- const captured = captureAutoCase({
659
- baseDir: engine.baseDir,
660
- rubricKey: config.rubricKey,
661
- source: "gate_no_consent",
662
- sessionId,
663
- summary: finalProposal.summary,
664
- reasons: [review.rationale],
665
- });
666
- logger.info(`auto-review auto-case captured (${reason}) [${sessionId}]: ${captured.caseId}`);
667
- }
668
- catch (cause) {
669
- logger.warn(`auto-case capture failed for ${sessionId}: ${cause instanceof Error ? cause.message : String(cause)}`);
670
- }
671
- }
672
- record({ sessionId, reason, turnsSinceLastReview, outcome: "declined", rationale: `${review.rationale}${withheld}` });
673
- return;
674
- }
675
- const source = { sessionId, ...(snapshot.sourceSeqs.length > 0 ? { seqs: [...snapshot.sourceSeqs] } : {}) };
676
- const result = engine.apply("local", sessionId, finalProposal, {
677
- scope: "local",
678
- baselineState: localState,
679
- ...(source ? { source } : {}),
680
- });
681
- logger.info(`auto-review approved (${reason}) [${sessionId}] after ${turnsSinceLastReview} turns; auto-refine ${result.id}: ${result.appliedEdits.filter((e) => e.applied).length} applied, ${result.appliedEdits.filter((e) => !e.applied).length} failed — ${review.rationale}`);
682
- record({ sessionId, reason, turnsSinceLastReview, outcome: "approved", rationale: review.rationale, refinementId: result.id });
683
- // Gap C4: emit structured evolve_complete event for third-party consumers.
684
- emitEvolveComplete(engine.baseDir, buildEvolveCompleteEvent(result, `auto_review:${reason}`, sessionId), config.reviewsRetain ?? DEFAULT_REVIEWS_RETAIN);
685
- // Visibility: tell the user what the gate just persisted. Only the
686
- // turn snapshot path notifies — a compaction-triggered gate must not wake
687
- // the agent mid-compaction — and only when something was actually applied
688
- // (a notice for zero edits is noise). Failure is contained in notifyAutoReview.
689
- if (config.notifyOnAutoReview && reason === "turn_snapshot" && result.appliedEdits.some((e) => e.applied)) {
690
- notifyAutoReview(ctx, agent, result, turnsSinceLastReview);
691
- }
692
- }
693
- /** True when the general planner returned only edits owned by the memory phase. */
694
- export function isDedicatedMemoryOnlyProposal(proposal) {
695
- return proposal.edits.length > 0 && proposal.edits.every((edit) => edit.kind === "memory");
696
- }
697
- /**
698
- * Remove memory edits from the general auto-review planner. The dedicated
699
- * memory phase owns this responsibility and has already run against the same
700
- * snapshot; accepting a second memory proposal would reintroduce duplicates.
701
- */
702
- export function stripDedicatedMemoryEdits(proposal) {
703
- const edits = proposal.edits.filter((edit) => edit.kind !== "memory");
704
- if (edits.length === proposal.edits.length)
705
- return proposal;
706
- return {
707
- ...proposal,
708
- edits,
709
- summary: `${proposal.summary} (memory edits owned by dedicated extractor)`,
710
- };
711
- }
712
- /**
713
- * Split a proposal into skill edits and everything else. Skill edits are the
714
- * governed part: they need explicit user consent before the gate applies
715
- * them, while the remaining edits flow through the normal auto path.
716
- */
717
- export function splitSkillEdits(proposal) {
718
- return {
719
- skillEdits: proposal.edits.filter((edit) => edit.kind === "skill"),
720
- otherEdits: proposal.edits.filter((edit) => edit.kind !== "skill"),
721
- };
722
- }
723
- /**
724
- * Ask the user whether to solidify proposed skill edits (guidance or
725
- * executable) into the harness. Returns true when every skill edit is
726
- * consented. Never writes a skill silently:
727
- * - no question service available → false (conservative);
728
- * - the same candidate was rejected within the cooldown window → false
729
- * without asking again (no nagging);
730
- * - the user declines → false and the rejection is recorded for cooldown;
731
- * - the question call fails/aborts → false (conservative).
732
- */
733
- export async function consultSkillEdits(ctx, agent, skillEdits, gate, language) {
734
- if (skillEdits.length === 0)
735
- return true;
736
- const key = skillEdits.map((edit) => edit.id ?? slug(edit.title ?? edit.kind, edit.kind)).join("|");
737
- const lastReject = gate.skillRejects.get(key);
738
- if (lastReject !== undefined && gate.turns - lastReject < SKILL_CONSULT_COOLDOWN_TURNS) {
739
- return false;
740
- }
741
- const userQuestions = questionServiceOf(ctx);
742
- if (!userQuestions) {
743
- return false;
744
- }
745
- const lang = language ?? resolveDialogLanguage(ctx);
746
- const description = skillEdits
747
- .map((edit) => skillEditLine(edit.action, edit.title ?? edit.id ?? edit.kind, edit.skill_kind, lang))
748
- .join("\n");
749
- try {
750
- const copy = skillConsultCopy(description, lang);
751
- const answer = await userQuestions.ask({
752
- questions: [
753
- {
754
- id: "evolve-skill-consult",
755
- question: copy.question,
756
- options: copy.options,
757
- },
758
- ],
759
- agent,
760
- });
761
- const item = answer.answers?.find((entry) => entry.id === "evolve-skill-consult");
762
- const selected = item?.selected ?? [];
763
- const consented = selected.includes("固化") || selected.includes("Solidify");
764
- if (!consented) {
765
- gate.skillRejects.set(key, gate.turns);
766
- }
767
- return consented;
768
- }
769
- catch {
770
- return false;
771
- }
772
- }
773
- //# sourceMappingURL=auto.js.map