dsh-continual-evolve 0.7.0 → 0.7.2

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/lib/command.d.ts CHANGED
@@ -17,6 +17,11 @@ export interface CommandRuntimeOptions {
17
17
  autoCase: boolean;
18
18
  /** Mechanical promotion guards for wrapup/fate (2026-08-22 policy). */
19
19
  promotionPolicy: PromotionPolicy;
20
+ /**
21
+ * Initial auto-review default from plugin config. The listener is always
22
+ * registered; this is not a registration gate.
23
+ */
24
+ autoReview?: boolean;
20
25
  }
21
26
  export declare function registerEvolveCommand(ctx: Context, engine: EvolutionEngine, opts: CommandGateOptions, runtime: CommandRuntimeOptions): void;
22
27
  /**
package/lib/command.js CHANGED
@@ -5,7 +5,8 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
5
5
  import { join } from "node:path";
6
6
  import { requireGlobalApproval } from "./approval.js";
7
7
  import { saveHarnessState } from "./state.js";
8
- import { appendResult, storePaths } from "./store.js";
8
+ import { appendResult, pruneJsonlFile, storePaths } from "./store.js";
9
+ import { DEFAULT_REFINEMENTS_RETAIN } from "./store.js";
9
10
  import { entrySourceOf } from "./source.js";
10
11
  import { filterLogBySession, formatLogLine, pluginLogFilePath } from "./logfile.js";
11
12
  import { collectFailureSummary, formatFailureSummary } from "./failures.js";
@@ -14,8 +15,10 @@ import { executeMountCommand, executeUnmountCommand } from "./mount-command.js";
14
15
  import { executeBenchmarkCommand } from "./benchmark-command.js";
15
16
  import { executeWrapupCommand } from "./wrapup-command.js";
16
17
  import { projectKeyOf } from "./project.js";
17
- import { loadUsage } from "./usage.js";
18
+ import { loadUsage, getUsageCount } from "./usage.js";
19
+ import { loadGateRuntime, saveGateRuntime } from "./runtime.js";
18
20
  import { planConsolidation } from "./consolidate.js";
21
+ import { loadTokenUsage, renderTokenUsageReport } from "./token-usage.js";
19
22
  const USAGE = `Usage:
20
23
  /evolve show this help and the current local store
21
24
  /evolve list [project|global] list entries (default local staging; "project" = this project's cross-session store, "global" = cross-project store)
@@ -40,7 +43,10 @@ const USAGE = `Usage:
40
43
  /evolve unmount <id> remove a hot-mounted plugin
41
44
  /evolve goal show the evolution goal (round-driven auto-review)
42
45
  /evolve goal <objective> create/update the evolution goal
43
- /evolve goal done complete the evolution goal`;
46
+ /evolve goal done complete the evolution goal
47
+ /evolve pause | resume pause/resume the auto-review gate (manual tools and commands keep working)
48
+ /evolve status gate state (patch flag + runtime switch) plus store entry counts
49
+ /evolve usage injection counts + exact direct-call token usage (benchmark subagents excluded)`;
44
50
  export function registerEvolveCommand(ctx, engine, opts, runtime) {
45
51
  ctx.commands.register({
46
52
  name: "evolve",
@@ -323,6 +329,14 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
323
329
  appendResult(paths, result);
324
330
  }
325
331
  }
332
+ // Storage hygiene (#20): an imported history obeys the
333
+ // same tail budget as a live one.
334
+ try {
335
+ pruneJsonlFile(paths.resultsPath, engine.retention?.refinements ?? DEFAULT_REFINEMENTS_RETAIN);
336
+ }
337
+ catch {
338
+ // ignored — the next apply retries
339
+ }
326
340
  }
327
341
  return success(`imported ${scope} store from ${path}`);
328
342
  }
@@ -338,6 +352,12 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
338
352
  ...(instructions ? { instructions } : {}),
339
353
  global: scope === "global",
340
354
  signal: invocation.signal,
355
+ tokenUsage: {
356
+ baseDir: engine.baseDir,
357
+ sessionId,
358
+ retain: engine.retention.tokenUsage,
359
+ onError: (cause) => ctx.logger("continual-evolve").warn(`token-usage ledger failed for ${sessionId}: ${cause instanceof Error ? cause.message : String(cause)}`),
360
+ },
341
361
  // skill-creator template facts (fallback: builtin guide).
342
362
  skillsRoot: join(engine.baseDir, "skills"),
343
363
  });
@@ -354,6 +374,31 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
354
374
  case "wrapup": {
355
375
  return await executeWrapupCommand(ctx, engine, invocation, runtime.promotionPolicy);
356
376
  }
377
+ case "pause":
378
+ case "resume": {
379
+ // The switch is independent of the static registration flag. A
380
+ // resume explicitly enables automatic snapshots; a pause leaves
381
+ // the enable bit intact so status explains the paused state.
382
+ const pausing = sub === "pause";
383
+ const defaultEnabled = runtime.autoReview ?? false;
384
+ const current = loadGateRuntime(engine.baseDir, defaultEnabled);
385
+ if (pausing && current.paused) {
386
+ return success("auto-review gate is already paused (no change).");
387
+ }
388
+ if (!pausing && current.enabled && !current.paused) {
389
+ return success("auto-review gate is already running (no change).");
390
+ }
391
+ saveGateRuntime(engine.baseDir, pausing, pausing ? current.enabled : true);
392
+ return success(pausing
393
+ ? "auto-review gate paused: no automatic snapshots, fate assessments, or gate LLM calls until /evolve resume. Manual evolve_* tools and /evolve commands keep working."
394
+ : "auto-review gate resumed: successful turns will produce incremental snapshots immediately.");
395
+ }
396
+ case "status": {
397
+ return success(renderGateStatus(engine, sessionId, projectKeyOf(invocation.agent), runtime));
398
+ }
399
+ case "usage": {
400
+ return success(renderUsageReport(engine, sessionId, projectKeyOf(invocation.agent)));
401
+ }
357
402
  case "goal": {
358
403
  return executeGoalCommand(ctx, invocation, rest);
359
404
  }
@@ -412,6 +457,99 @@ function demoteEntry(engine, id, sessionId, projectKey) {
412
457
  }
413
458
  return error(`entry ${id} not found in the global, project, or local store`);
414
459
  }
460
+ /**
461
+ * Status answers whether the always-registered listener is enabled at runtime
462
+ * and what stores/retention it currently sees.
463
+ */
464
+ function renderGateStatus(engine, sessionId, projectKey, runtime) {
465
+ const configuredDefault = runtime.autoReview === undefined ? "unknown" : runtime.autoReview ? "on" : "off";
466
+ const current = loadGateRuntime(engine.baseDir, runtime.autoReview ?? false);
467
+ const effective = current.paused ? "PAUSED (resume with /evolve resume)" : current.enabled ? "running" : "off (enable with /evolve resume)";
468
+ const gateLine = `gate: listener registered · config default ${configuredDefault} · runtime ${effective}`;
469
+ const countEntries = (state) => Object.values(state.entries).reduce((n, byKind) => n + Object.keys(byKind).length, 0);
470
+ const lines = [gateLine];
471
+ lines.push(`stores: global ${countEntries(engine.load("global", undefined))} entries · local(${sessionId}) ${countEntries(engine.load("local", sessionId))} entries`);
472
+ if (projectKey) {
473
+ try {
474
+ lines[lines.length - 1] += ` · project ${countEntries(engine.load("project", projectKey))} entries`;
475
+ }
476
+ catch {
477
+ lines[lines.length - 1] += " · project (unavailable)";
478
+ }
479
+ }
480
+ const retention = engine.retention;
481
+ if (retention) {
482
+ lines.push(`retention: snapshots ${retention.snapshots} · refinements ${retention.refinements} · reviews ${retention.reviews} · token usage ${retention.tokenUsage} (historyRetain)`);
483
+ }
484
+ return lines.join("\n");
485
+ }
486
+ /**
487
+ * #21 P0 usage ledger: injection counts per entry across the stores the
488
+ * human can see (global + this session + this project). Counts are
489
+ * per-session since the v2 usage shape — "in how many sessions did this
490
+ * entry surface" — the exposure half of the #18a归零实验 verdict.
491
+ */
492
+ function renderUsageReport(engine, sessionId, projectKey) {
493
+ const store = loadUsage(engine.baseDir);
494
+ const rows = [];
495
+ const seen = new Set();
496
+ const collect = (state) => {
497
+ for (const kind of Object.keys(state.entries)) {
498
+ for (const entry of Object.values(state.entries[kind])) {
499
+ const key = `${kind}:${entry.id}`;
500
+ if (seen.has(key))
501
+ continue;
502
+ seen.add(key);
503
+ const count = getUsageCount(store, kind, entry.id);
504
+ rows.push({
505
+ key,
506
+ title: entry.title,
507
+ count,
508
+ ...(store.lastSession?.[`${kind}:${entry.id}`] ? { lastSession: store.lastSession[`${kind}:${entry.id}`] } : {}),
509
+ });
510
+ }
511
+ }
512
+ };
513
+ collect(engine.load("global", undefined));
514
+ collect(engine.load("local", sessionId));
515
+ if (projectKey) {
516
+ try {
517
+ collect(engine.load("project", projectKey));
518
+ }
519
+ catch {
520
+ // project store unavailable here — global+local still report
521
+ }
522
+ }
523
+ const liveKeys = new Set(rows.map((r) => r.key));
524
+ const orphaned = Object.keys(store.counts).filter((k) => !liveKeys.has(k)).length;
525
+ const total = rows.reduce((n, r) => n + r.count, 0);
526
+ const injected = rows.filter((r) => r.count > 0).sort((a, b) => b.count - a.count || (a.key < b.key ? -1 : 1));
527
+ const stale = rows.filter((r) => r.count === 0).sort((a, b) => (a.key < b.key ? -1 : 1));
528
+ const lines = [`injected entries: ${total} injections across ${injected.length} of ${rows.length} stored entries${orphaned > 0 ? ` (+${orphaned} historical key(s) for deleted entries)` : ""}`];
529
+ if (injected.length > 0) {
530
+ lines.push("injected (top 15):");
531
+ for (const row of injected.slice(0, 15)) {
532
+ lines.push(` ${row.key} — ${row.count}× · ${row.title}${row.lastSession ? ` (last in ${row.lastSession})` : ""}`);
533
+ }
534
+ if (injected.length > 15) {
535
+ lines.push(` … and ${injected.length - 15} more`);
536
+ }
537
+ }
538
+ else {
539
+ lines.push("injected: (none yet — nothing has surfaced into a session prompt)");
540
+ }
541
+ if (stale.length > 0) {
542
+ lines.push(`never injected (${stale.length}):`);
543
+ for (const row of stale.slice(0, 20)) {
544
+ lines.push(` ${row.key} · ${row.title}`);
545
+ }
546
+ if (stale.length > 20) {
547
+ lines.push(` … and ${stale.length - 20} more`);
548
+ }
549
+ }
550
+ lines.push("", ...renderTokenUsageReport(loadTokenUsage(engine.baseDir), engine.retention.tokenUsage));
551
+ return lines.join("\n");
552
+ }
415
553
  function renderResult(result) {
416
554
  const applied = result.appliedEdits.filter((e) => e.applied);
417
555
  const failed = result.appliedEdits.filter((e) => !e.applied);
@@ -32,7 +32,9 @@ export declare function buildEvolveCompleteEvent(result: RefinementResult, trigg
32
32
  /**
33
33
  * Emit an evolve_complete event to the reviews.jsonl audit trail. The event
34
34
  * is JSONL-formatted (one line) so consumers can tail and parse it. This is
35
- * a best-effort write — failure never blocks the refinement path.
35
+ * a best-effort write — failure never blocks the refinement path. Storage
36
+ * hygiene (#20): the trail is truncated to its tail after the append so the
37
+ * audit file cannot grow without bound; readers only need a recent window.
36
38
  */
37
- export declare function emitEvolveComplete(baseDir: string, event: EvolveCompleteEvent): void;
39
+ export declare function emitEvolveComplete(baseDir: string, event: EvolveCompleteEvent, reviewsRetain?: number): void;
38
40
  //# sourceMappingURL=evolve-event.d.ts.map
@@ -11,6 +11,7 @@
11
11
  */
12
12
  import { appendFileSync, mkdirSync } from "node:fs";
13
13
  import { join } from "node:path";
14
+ import { DEFAULT_REVIEWS_RETAIN, pruneJsonlFile, reviewsPath } from "./store.js";
14
15
  /** Build a structured evolve-complete event from a refinement result. */
15
16
  export function buildEvolveCompleteEvent(result, trigger, sessionId) {
16
17
  return {
@@ -34,9 +35,11 @@ export function buildEvolveCompleteEvent(result, trigger, sessionId) {
34
35
  /**
35
36
  * Emit an evolve_complete event to the reviews.jsonl audit trail. The event
36
37
  * is JSONL-formatted (one line) so consumers can tail and parse it. This is
37
- * a best-effort write — failure never blocks the refinement path.
38
+ * a best-effort write — failure never blocks the refinement path. Storage
39
+ * hygiene (#20): the trail is truncated to its tail after the append so the
40
+ * audit file cannot grow without bound; readers only need a recent window.
38
41
  */
39
- export function emitEvolveComplete(baseDir, event) {
42
+ export function emitEvolveComplete(baseDir, event, reviewsRetain = DEFAULT_REVIEWS_RETAIN) {
40
43
  try {
41
44
  const dir = join(baseDir, "evolve");
42
45
  mkdirSync(dir, { recursive: true });
@@ -44,6 +47,13 @@ export function emitEvolveComplete(baseDir, event) {
44
47
  }
45
48
  catch {
46
49
  // Event emission is diagnostic; never interrupt the refinement path.
50
+ return;
51
+ }
52
+ try {
53
+ pruneJsonlFile(reviewsPath(baseDir), reviewsRetain);
54
+ }
55
+ catch {
56
+ // ignored — the next emission retries
47
57
  }
48
58
  }
49
59
  //# sourceMappingURL=evolve-event.js.map
package/lib/fate.d.ts CHANGED
@@ -119,7 +119,7 @@ export interface FateApplyResult {
119
119
  export declare function applyLocalFates(engine: EvolutionEngine, sessionId: string, plan: FatePlan, localState: HarnessState, mode: FateApplyMode): FateApplyResult;
120
120
  /**
121
121
  * The gate's local-fate phase. Runs after the review phase on every gate
122
- * trigger (turn_interval / compact), subject to cadence and cooldown. All
122
+ * trigger (turn_snapshot / compact), subject to cadence and cooldown. All
123
123
  * failures are contained and recorded — a broken fate dimension never
124
124
  * disturbs the agent loop.
125
125
  */
package/lib/fate.js CHANGED
@@ -192,7 +192,7 @@ export function applyLocalFates(engine, sessionId, plan, localState, mode) {
192
192
  }
193
193
  /**
194
194
  * The gate's local-fate phase. Runs after the review phase on every gate
195
- * trigger (turn_interval / compact), subject to cadence and cooldown. All
195
+ * trigger (turn_snapshot / compact), subject to cadence and cooldown. All
196
196
  * failures are contained and recorded — a broken fate dimension never
197
197
  * disturbs the agent loop.
198
198
  */
@@ -201,6 +201,12 @@ export async function runLocalFatePhase(ctx, engine, agent, config, state, reaso
201
201
  return;
202
202
  const logger = ctx.logger("continual-evolve");
203
203
  const sessionId = agent.id;
204
+ const tokenUsage = {
205
+ baseDir: engine.baseDir,
206
+ sessionId,
207
+ retain: engine.retention.tokenUsage,
208
+ onError: (cause) => logger.warn(`token-usage ledger failed for ${sessionId}: ${cause instanceof Error ? cause.message : String(cause)}`),
209
+ };
204
210
  const localState = engine.load("local", sessionId);
205
211
  const globalState = engine.load("global", undefined);
206
212
  const candidates = listLocalCandidates(localState, globalState, engine.baseDir);
@@ -218,7 +224,7 @@ export async function runLocalFatePhase(ctx, engine, agent, config, state, reaso
218
224
  state.lastFateAt = state.turns;
219
225
  let assessment;
220
226
  try {
221
- assessment = await assessLocalEntries(ctx, agent, candidates);
227
+ assessment = await assessLocalEntries(ctx, agent, candidates, { tokenUsage, tokenUsagePhase: "fate" });
222
228
  }
223
229
  catch (cause) {
224
230
  const message = cause instanceof Error ? cause.message : String(cause);
@@ -249,7 +255,7 @@ export async function runLocalFatePhase(ctx, engine, agent, config, state, reaso
249
255
  rationale: `fate: ${assessment.rationale} (${applied.join("; ")})`,
250
256
  refinementId: results.map((result) => result.id).join(","),
251
257
  });
252
- if (config.notifyOnAutoReview && reason === "turn_interval" && applied.length > 0) {
258
+ if (config.notifyOnAutoReview && (reason === "turn_snapshot" || reason === "turn_interval") && applied.length > 0) {
253
259
  notifyFateApplied(ctx, agent, applied);
254
260
  }
255
261
  return;
@@ -303,7 +309,7 @@ export async function runLocalFatePhase(ctx, engine, agent, config, state, reaso
303
309
  rationale: `fate: ${assessment.rationale} (${applied.join("; ")})`,
304
310
  refinementId: results.map((result) => result.id).join(","),
305
311
  });
306
- if (config.notifyOnAutoReview && reason === "turn_interval") {
312
+ if (config.notifyOnAutoReview && (reason === "turn_snapshot" || reason === "turn_interval")) {
307
313
  notifyFateApplied(ctx, agent, applied);
308
314
  }
309
315
  return;
@@ -327,7 +333,7 @@ export function buildFateNotice(applied) {
327
333
  "请用一句话简短确认即可,不要调用任何工具。",
328
334
  ].join("\n");
329
335
  }
330
- /** Queue the follow-up notice turn (turn_interval only, like the review notice). */
336
+ /** Queue the follow-up notice turn (successful-turn path only). */
331
337
  function notifyFateApplied(ctx, agent, applied) {
332
338
  try {
333
339
  agent.followup(createUserMessage({
package/lib/index.d.ts CHANGED
@@ -10,14 +10,23 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
10
10
  baseDir: z<string, string, "plain">;
11
11
  /** System-prompt section order for the evolution guidance. */
12
12
  sectionOrder: z<number, number, "defined">;
13
- /** Enable the automatic review gate (off by default: it costs model calls). */
13
+ /** Initial automatic-review default when no runtime switch exists (off by default: it costs model calls). */
14
14
  autoReview: z<boolean, boolean, "defined">;
15
- /** Gate runs when this many turns have passed since the last review. */
15
+ /** Legacy/local-fate cadence fallback; successful-turn review is snapshot-driven. */
16
16
  reviewIntervalTurns: z<number, number, "defined">;
17
17
  /** Trajectory slice handed to the gate, in characters. */
18
18
  maxReviewInputChars: z<number, number, "defined">;
19
19
  /** Output budget for the cheap gate call. */
20
20
  reviewBudgetTokens: z<number, number, "defined">;
21
+ /**
22
+ * Planner prefix-cache routing for the gate and planner inputs: `auto`
23
+ * prepends a session-derived message prefix when the session shows
24
+ * cache-read evidence (dropping the redundant flat trajectory text),
25
+ * `session` always prefixes, `off` keeps the legacy flat-text input.
26
+ */
27
+ plannerPrefixCache: z<"session" | "off" | "auto", "session" | "off" | "auto", "defined">;
28
+ /** Session-prefix budget for Route A planning inputs, in characters. */
29
+ plannerPrefixMaxChars: z<number, number, "defined">;
21
30
  /** After an approved gate run with applied edits, queue a visible follow-up notice. */
22
31
  notifyOnAutoReview: z<boolean, boolean, "defined">;
23
32
  /** Cross-session (global) edits require an explicit human approval. */
@@ -74,19 +83,47 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
74
83
  promotionMinChars: z<number, number, "defined">;
75
84
  /** Entry-directory lines injected per build before folding into a counter. */
76
85
  injectionDirectoryLines: z<number, number, "defined">;
86
+ /**
87
+ * Storage hygiene (#20): how much append-only past each write keeps.
88
+ * `snapshots` = full-state copies per store, `refinements` = tail lines
89
+ * per store history, `reviews` = tail lines of the shared audit trail,
90
+ * `tokenUsage` = tail lines of the direct-call token ledger.
91
+ * Absent fields fall back to the store defaults (20 / 500 / 500 / 500) —
92
+ * rollback needs recent snapshots, readers need a recent window.
93
+ */
94
+ historyRetain: z<Schemastery.ObjectS<NoInfer<{
95
+ snapshots: z<number, number, "plain">;
96
+ refinements: z<number, number, "plain">;
97
+ reviews: z<number, number, "plain">;
98
+ tokenUsage: z<number, number, "plain">;
99
+ }>>, Schemastery.ObjectT<NoInfer<{
100
+ snapshots: z<number, number, "plain">;
101
+ refinements: z<number, number, "plain">;
102
+ reviews: z<number, number, "plain">;
103
+ tokenUsage: z<number, number, "plain">;
104
+ }>>, "plain">;
77
105
  }>>, Schemastery.ObjectT<NoInfer<{
78
106
  /** Root for evolution stores; defaults to the resolved DSH home. */
79
107
  baseDir: z<string, string, "plain">;
80
108
  /** System-prompt section order for the evolution guidance. */
81
109
  sectionOrder: z<number, number, "defined">;
82
- /** Enable the automatic review gate (off by default: it costs model calls). */
110
+ /** Initial automatic-review default when no runtime switch exists (off by default: it costs model calls). */
83
111
  autoReview: z<boolean, boolean, "defined">;
84
- /** Gate runs when this many turns have passed since the last review. */
112
+ /** Legacy/local-fate cadence fallback; successful-turn review is snapshot-driven. */
85
113
  reviewIntervalTurns: z<number, number, "defined">;
86
114
  /** Trajectory slice handed to the gate, in characters. */
87
115
  maxReviewInputChars: z<number, number, "defined">;
88
116
  /** Output budget for the cheap gate call. */
89
117
  reviewBudgetTokens: z<number, number, "defined">;
118
+ /**
119
+ * Planner prefix-cache routing for the gate and planner inputs: `auto`
120
+ * prepends a session-derived message prefix when the session shows
121
+ * cache-read evidence (dropping the redundant flat trajectory text),
122
+ * `session` always prefixes, `off` keeps the legacy flat-text input.
123
+ */
124
+ plannerPrefixCache: z<"session" | "off" | "auto", "session" | "off" | "auto", "defined">;
125
+ /** Session-prefix budget for Route A planning inputs, in characters. */
126
+ plannerPrefixMaxChars: z<number, number, "defined">;
90
127
  /** After an approved gate run with applied edits, queue a visible follow-up notice. */
91
128
  notifyOnAutoReview: z<boolean, boolean, "defined">;
92
129
  /** Cross-session (global) edits require an explicit human approval. */
@@ -143,6 +180,25 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
143
180
  promotionMinChars: z<number, number, "defined">;
144
181
  /** Entry-directory lines injected per build before folding into a counter. */
145
182
  injectionDirectoryLines: z<number, number, "defined">;
183
+ /**
184
+ * Storage hygiene (#20): how much append-only past each write keeps.
185
+ * `snapshots` = full-state copies per store, `refinements` = tail lines
186
+ * per store history, `reviews` = tail lines of the shared audit trail,
187
+ * `tokenUsage` = tail lines of the direct-call token ledger.
188
+ * Absent fields fall back to the store defaults (20 / 500 / 500 / 500) —
189
+ * rollback needs recent snapshots, readers need a recent window.
190
+ */
191
+ historyRetain: z<Schemastery.ObjectS<NoInfer<{
192
+ snapshots: z<number, number, "plain">;
193
+ refinements: z<number, number, "plain">;
194
+ reviews: z<number, number, "plain">;
195
+ tokenUsage: z<number, number, "plain">;
196
+ }>>, Schemastery.ObjectT<NoInfer<{
197
+ snapshots: z<number, number, "plain">;
198
+ refinements: z<number, number, "plain">;
199
+ reviews: z<number, number, "plain">;
200
+ tokenUsage: z<number, number, "plain">;
201
+ }>>, "plain">;
146
202
  }>>, "plain">;
147
203
  /**
148
204
  * Structurally typed resolved config (loader passes the validated object).
package/lib/index.js CHANGED
@@ -3,9 +3,9 @@
3
3
  *
4
4
  * Mounts the evolution engine, registers the model-facing evolve_* tools,
5
5
  * the human-facing /evolve command, the system-prompt guidance section, and
6
- * (opt-in) the automatic review gate that runs the planner on a turn
7
- * interval. Store roots default under the resolved DSH home; a deployment
8
- * may override `baseDir` in the plugin config.
6
+ * the automatic review gate. The listener is always registered, while its
7
+ * effective state is controlled by the runtime switch. Successful turns feed
8
+ * incremental snapshots through a per-session serial scheduler.
9
9
  */
10
10
  import { join } from "node:path";
11
11
  import z from "@deepseek-ai/schemastery";
@@ -29,14 +29,23 @@ export const Config = z.object({
29
29
  baseDir: z.string(),
30
30
  /** System-prompt section order for the evolution guidance. */
31
31
  sectionOrder: z.natural().default(118),
32
- /** Enable the automatic review gate (off by default: it costs model calls). */
32
+ /** Initial automatic-review default when no runtime switch exists (off by default: it costs model calls). */
33
33
  autoReview: z.boolean().default(false),
34
- /** Gate runs when this many turns have passed since the last review. */
34
+ /** Legacy/local-fate cadence fallback; successful-turn review is snapshot-driven. */
35
35
  reviewIntervalTurns: z.natural().default(6),
36
36
  /** Trajectory slice handed to the gate, in characters. */
37
37
  maxReviewInputChars: z.natural().default(40000),
38
38
  /** Output budget for the cheap gate call. */
39
39
  reviewBudgetTokens: z.natural().default(4096),
40
+ /**
41
+ * Planner prefix-cache routing for the gate and planner inputs: `auto`
42
+ * prepends a session-derived message prefix when the session shows
43
+ * cache-read evidence (dropping the redundant flat trajectory text),
44
+ * `session` always prefixes, `off` keeps the legacy flat-text input.
45
+ */
46
+ plannerPrefixCache: z.union([z.const("auto"), z.const("session"), z.const("off")]).default("auto"),
47
+ /** Session-prefix budget for Route A planning inputs, in characters. */
48
+ plannerPrefixMaxChars: z.natural().default(12000),
40
49
  /** After an approved gate run with applied edits, queue a visible follow-up notice. */
41
50
  notifyOnAutoReview: z.boolean().default(true),
42
51
  /** Cross-session (global) edits require an explicit human approval. */
@@ -93,6 +102,20 @@ export const Config = z.object({
93
102
  promotionMinChars: z.natural().default(100),
94
103
  /** Entry-directory lines injected per build before folding into a counter. */
95
104
  injectionDirectoryLines: z.natural().default(15),
105
+ /**
106
+ * Storage hygiene (#20): how much append-only past each write keeps.
107
+ * `snapshots` = full-state copies per store, `refinements` = tail lines
108
+ * per store history, `reviews` = tail lines of the shared audit trail,
109
+ * `tokenUsage` = tail lines of the direct-call token ledger.
110
+ * Absent fields fall back to the store defaults (20 / 500 / 500 / 500) —
111
+ * rollback needs recent snapshots, readers need a recent window.
112
+ */
113
+ historyRetain: z.object({
114
+ snapshots: z.natural(),
115
+ refinements: z.natural(),
116
+ reviews: z.natural(),
117
+ tokenUsage: z.natural(),
118
+ }),
96
119
  });
97
120
  export function apply(ctx, config) {
98
121
  const baseDir = resolveDshHome(config.baseDir);
@@ -111,7 +134,7 @@ export function apply(ctx, config) {
111
134
  .warn(`skill materialization failed for ${result.id}: ${cause instanceof Error ? cause.message : String(cause)}`);
112
135
  }
113
136
  },
114
- });
137
+ }, config.historyRetain ? { historyRetain: { ...config.historyRetain } } : {});
115
138
  ctx.provide(EVOLUTION_SERVICE, { engine, baseDir });
116
139
  ctx.systemPrompt.section({
117
140
  name: "tool:continual-evolve",
@@ -139,6 +162,7 @@ export function apply(ctx, config) {
139
162
  autoRollbackOnReject: config.autoRollbackOnReject ?? true,
140
163
  autoCase: config.autoCase ?? true,
141
164
  promotionPolicy,
165
+ ...(config.autoReview !== undefined ? { autoReview: config.autoReview } : {}),
142
166
  });
143
167
  // Plugin-owned file logging: every cordis log message lands in
144
168
  // <baseDir>/evolve/plugin.log regardless of how dsh web was launched —
@@ -153,22 +177,28 @@ export function apply(ctx, config) {
153
177
  void restoreMounted(ctx, baseDir).catch((cause) => {
154
178
  ctx.logger("continual-evolve").warn(`mount restore failed: ${cause instanceof Error ? cause.message : String(cause)}`);
155
179
  });
156
- if (config.autoReview) {
157
- registerAutoReview(ctx, engine, {
158
- intervalTurns: config.reviewIntervalTurns ?? 6,
159
- maxInputChars: config.maxReviewInputChars ?? 40000,
160
- budgetTokens: config.reviewBudgetTokens ?? 4096,
161
- notifyOnAutoReview: config.notifyOnAutoReview ?? true,
162
- localFate: config.localFate ?? true,
163
- fateIntervalTurns: config.fateIntervalTurns ?? config.reviewIntervalTurns ?? 6,
164
- goalBlockedWrapupTurns: config.goalBlockedWrapupTurns ?? 3,
165
- promotionPolicy,
166
- autoCase: config.autoCase ?? true,
167
- rubricKey,
168
- ...(config.reviewModel ? { reviewModel: config.reviewModel } : {}),
169
- });
170
- ctx.logger("continual-evolve").info(`continual-evolve auto-review enabled (every ${config.reviewIntervalTurns ?? 6} turns; local-fate ${config.localFate ?? true ? "on" : "off"} every ${config.fateIntervalTurns ?? config.reviewIntervalTurns ?? 6} turns)`);
171
- }
180
+ // The listener is always registered. `autoReview` is only the initial
181
+ // runtime default; `/evolve resume` can enable it without a profile edit
182
+ // or process restart, while manual tools and commands stay independent.
183
+ registerAutoReview(ctx, engine, {
184
+ intervalTurns: config.reviewIntervalTurns ?? 6,
185
+ enabledByDefault: config.autoReview ?? false,
186
+ maxInputChars: config.maxReviewInputChars ?? 40000,
187
+ budgetTokens: config.reviewBudgetTokens ?? 4096,
188
+ notifyOnAutoReview: config.notifyOnAutoReview ?? true,
189
+ localFate: config.localFate ?? true,
190
+ fateIntervalTurns: config.fateIntervalTurns ?? config.reviewIntervalTurns ?? 6,
191
+ goalBlockedWrapupTurns: config.goalBlockedWrapupTurns ?? 3,
192
+ promotionPolicy,
193
+ autoCase: config.autoCase ?? true,
194
+ rubricKey,
195
+ ...(config.historyRetain?.reviews !== undefined ? { reviewsRetain: config.historyRetain.reviews } : {}),
196
+ ...(config.reviewModel ? { reviewModel: config.reviewModel } : {}),
197
+ ...(config.plannerPrefixCache ? { prefixCacheMode: config.plannerPrefixCache } : {}),
198
+ ...(config.plannerPrefixMaxChars !== undefined ? { prefixMaxChars: config.plannerPrefixMaxChars } : {}),
199
+ });
200
+ const runtimeDefault = config.autoReview ?? false;
201
+ ctx.logger("continual-evolve").info(`continual-evolve auto-review registered (default ${runtimeDefault ? "on" : "off"}; successful-turn snapshots; local-fate ${config.localFate ?? true ? "on" : "off"} every ${config.fateIntervalTurns ?? config.reviewIntervalTurns ?? 6} turns)`);
172
202
  ctx.logger("continual-evolve").info(`continual-evolve mounted (baseDir=${baseDir})`);
173
203
  }
174
204
  //# sourceMappingURL=index.js.map
package/lib/llm-text.d.ts CHANGED
@@ -9,6 +9,16 @@
9
9
  * and JSON parsing.
10
10
  */
11
11
  import type { Context } from "@deepseek-ai/cordis";
12
+ import { type Message, type TokenUsage } from "@deepseek-ai/dsh-llm";
13
+ /** Terminal outcome of one shared direct LLM call. */
14
+ export type StreamTextOutcome = "success" | "max-tokens" | "error" | "aborted" | "empty";
15
+ /** Provider usage and terminal result observed after the stream settles. */
16
+ export interface StreamTextObservation {
17
+ usage?: TokenUsage;
18
+ outcome: StreamTextOutcome;
19
+ }
20
+ /** Diagnostic observer invoked before streamText returns or throws. */
21
+ export type StreamTextUsageObserver = (observation: StreamTextObservation) => void;
12
22
  export interface StreamTextOptions {
13
23
  provider: string;
14
24
  model: string;
@@ -16,6 +26,19 @@ export interface StreamTextOptions {
16
26
  prompt: string;
17
27
  maxTokens?: number;
18
28
  signal?: AbortSignal | undefined;
29
+ /**
30
+ * Route A session prefix: session-derived messages sent before the
31
+ * trailing caller message so providers with prompt caching can serve
32
+ * the shared context at cache-read price. Empty/omitted keeps the
33
+ * legacy single-message request.
34
+ */
35
+ prefixMessages?: readonly Message[];
36
+ /**
37
+ * Observe the provider-reported usage and terminal outcome after the
38
+ * stream settles. The callback runs before success/error return and is
39
+ * diagnostic-only: throwing from it never changes the model call result.
40
+ */
41
+ onUsage?: StreamTextUsageObserver;
19
42
  }
20
43
  /**
21
44
  * Stream a single-turn text completion through `ctx.llm`. Forces