dsh-continual-evolve 0.11.0 → 0.12.1

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 (161) hide show
  1. package/README.md +42 -115
  2. package/README.zh.md +33 -93
  3. package/client/client.js +253 -0
  4. package/cordis.patch.yml +2 -2
  5. package/lib/card-routes.d.ts +32 -0
  6. package/lib/card-routes.js +92 -0
  7. package/lib/index.d.ts +65 -217
  8. package/lib/index.js +74 -234
  9. package/lib/known-workspaces.d.ts +22 -0
  10. package/lib/known-workspaces.js +42 -0
  11. package/lib/memory-guide.d.ts +13 -14
  12. package/lib/memory-guide.js +34 -21
  13. package/lib/memory-section.d.ts +77 -0
  14. package/lib/memory-section.js +189 -0
  15. package/lib/memory-snapshot.d.ts +54 -0
  16. package/lib/memory-snapshot.js +125 -0
  17. package/package.json +8 -17
  18. package/lib/apply.d.ts +0 -22
  19. package/lib/apply.js +0 -157
  20. package/lib/approval.d.ts +0 -73
  21. package/lib/approval.js +0 -127
  22. package/lib/audit-log.d.ts +0 -19
  23. package/lib/audit-log.js +0 -52
  24. package/lib/auto.d.ts +0 -236
  25. package/lib/auto.js +0 -773
  26. package/lib/autocase.d.ts +0 -38
  27. package/lib/autocase.js +0 -76
  28. package/lib/benchmark-command.d.ts +0 -9
  29. package/lib/benchmark-command.js +0 -357
  30. package/lib/benchmark.d.ts +0 -151
  31. package/lib/benchmark.js +0 -250
  32. package/lib/command-util.d.ts +0 -70
  33. package/lib/command-util.js +0 -123
  34. package/lib/command.d.ts +0 -13
  35. package/lib/command.js +0 -134
  36. package/lib/consolidate.d.ts +0 -90
  37. package/lib/consolidate.js +0 -180
  38. package/lib/copy.d.ts +0 -37
  39. package/lib/copy.js +0 -58
  40. package/lib/declines.d.ts +0 -103
  41. package/lib/declines.js +0 -179
  42. package/lib/evaluate.d.ts +0 -74
  43. package/lib/evaluate.js +0 -271
  44. package/lib/evolve-event.d.ts +0 -40
  45. package/lib/evolve-event.js +0 -60
  46. package/lib/extraction-phase.d.ts +0 -132
  47. package/lib/extraction-phase.js +0 -236
  48. package/lib/failures.d.ts +0 -42
  49. package/lib/failures.js +0 -171
  50. package/lib/fate.d.ts +0 -130
  51. package/lib/fate.js +0 -350
  52. package/lib/goal-command.d.ts +0 -7
  53. package/lib/goal-command.js +0 -37
  54. package/lib/goal.d.ts +0 -72
  55. package/lib/goal.js +0 -72
  56. package/lib/inject.d.ts +0 -90
  57. package/lib/inject.js +0 -164
  58. package/lib/injection-caps.d.ts +0 -4
  59. package/lib/injection-caps.js +0 -4
  60. package/lib/injection-directory.d.ts +0 -29
  61. package/lib/injection-directory.js +0 -78
  62. package/lib/injection-rank.d.ts +0 -85
  63. package/lib/injection-rank.js +0 -161
  64. package/lib/listener.d.ts +0 -45
  65. package/lib/listener.js +0 -199
  66. package/lib/llm-text.d.ts +0 -104
  67. package/lib/llm-text.js +0 -140
  68. package/lib/logfile.d.ts +0 -71
  69. package/lib/logfile.js +0 -159
  70. package/lib/maintenance-commands.d.ts +0 -23
  71. package/lib/maintenance-commands.js +0 -136
  72. package/lib/memory-agent.d.ts +0 -93
  73. package/lib/memory-agent.js +0 -300
  74. package/lib/memory-apply.d.ts +0 -44
  75. package/lib/memory-apply.js +0 -202
  76. package/lib/memory-benchmark.d.ts +0 -76
  77. package/lib/memory-benchmark.js +0 -170
  78. package/lib/memory-index.d.ts +0 -114
  79. package/lib/memory-index.js +0 -197
  80. package/lib/memory-manifest.d.ts +0 -16
  81. package/lib/memory-manifest.js +0 -70
  82. package/lib/message-source.d.ts +0 -23
  83. package/lib/message-source.js +0 -12
  84. package/lib/mount-command.d.ts +0 -10
  85. package/lib/mount-command.js +0 -48
  86. package/lib/mount.d.ts +0 -42
  87. package/lib/mount.js +0 -212
  88. package/lib/notify.d.ts +0 -48
  89. package/lib/notify.js +0 -57
  90. package/lib/plan-command.d.ts +0 -12
  91. package/lib/plan-command.js +0 -38
  92. package/lib/plan.d.ts +0 -16
  93. package/lib/plan.js +0 -126
  94. package/lib/planner.d.ts +0 -61
  95. package/lib/planner.js +0 -144
  96. package/lib/pool.d.ts +0 -7
  97. package/lib/pool.js +0 -25
  98. package/lib/prefix-cache.d.ts +0 -79
  99. package/lib/prefix-cache.js +0 -168
  100. package/lib/project.d.ts +0 -28
  101. package/lib/project.js +0 -66
  102. package/lib/projection.d.ts +0 -22
  103. package/lib/projection.js +0 -112
  104. package/lib/promotion.d.ts +0 -94
  105. package/lib/promotion.js +0 -171
  106. package/lib/recall.d.ts +0 -85
  107. package/lib/recall.js +0 -142
  108. package/lib/record-language.d.ts +0 -85
  109. package/lib/record-language.js +0 -152
  110. package/lib/render.d.ts +0 -13
  111. package/lib/render.js +0 -80
  112. package/lib/review-scheduler.d.ts +0 -38
  113. package/lib/review-scheduler.js +0 -183
  114. package/lib/review.d.ts +0 -60
  115. package/lib/review.js +0 -145
  116. package/lib/rollback.d.ts +0 -9
  117. package/lib/rollback.js +0 -68
  118. package/lib/rubric.d.ts +0 -29
  119. package/lib/rubric.js +0 -119
  120. package/lib/runtime-commands.d.ts +0 -14
  121. package/lib/runtime-commands.js +0 -115
  122. package/lib/runtime.d.ts +0 -24
  123. package/lib/runtime.js +0 -87
  124. package/lib/score.d.ts +0 -64
  125. package/lib/score.js +0 -191
  126. package/lib/search.d.ts +0 -83
  127. package/lib/search.js +0 -136
  128. package/lib/service.d.ts +0 -63
  129. package/lib/service.js +0 -253
  130. package/lib/skill-render.d.ts +0 -23
  131. package/lib/skill-render.js +0 -69
  132. package/lib/skill.d.ts +0 -15
  133. package/lib/skill.js +0 -80
  134. package/lib/skillquality.d.ts +0 -85
  135. package/lib/skillquality.js +0 -320
  136. package/lib/source.d.ts +0 -30
  137. package/lib/source.js +0 -43
  138. package/lib/state.d.ts +0 -26
  139. package/lib/state.js +0 -151
  140. package/lib/store.d.ts +0 -61
  141. package/lib/store.js +0 -164
  142. package/lib/token-usage.d.ts +0 -68
  143. package/lib/token-usage.js +0 -211
  144. package/lib/tool.d.ts +0 -34
  145. package/lib/tool.js +0 -320
  146. package/lib/turn-snapshot.d.ts +0 -78
  147. package/lib/turn-snapshot.js +0 -252
  148. package/lib/types.d.ts +0 -218
  149. package/lib/types.js +0 -133
  150. package/lib/usage.d.ts +0 -36
  151. package/lib/usage.js +0 -98
  152. package/lib/validate.d.ts +0 -28
  153. package/lib/validate.js +0 -181
  154. package/lib/wrapup-candidates.d.ts +0 -127
  155. package/lib/wrapup-candidates.js +0 -161
  156. package/lib/wrapup-command.d.ts +0 -10
  157. package/lib/wrapup-command.js +0 -243
  158. package/lib/wrapup-proposals.d.ts +0 -101
  159. package/lib/wrapup-proposals.js +0 -281
  160. package/lib/wrapup.d.ts +0 -56
  161. 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