dsh-continual-evolve 0.6.1 → 0.7.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.
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";
@@ -13,11 +14,14 @@ import { executeGoalCommand } from "./goal-command.js";
13
14
  import { executeMountCommand, executeUnmountCommand } from "./mount-command.js";
14
15
  import { executeBenchmarkCommand } from "./benchmark-command.js";
15
16
  import { executeWrapupCommand } from "./wrapup-command.js";
16
- import { loadUsage } from "./usage.js";
17
+ import { projectKeyOf } from "./project.js";
18
+ import { loadUsage, getUsageCount } from "./usage.js";
19
+ import { loadGateRuntime, saveGateRuntime } from "./runtime.js";
17
20
  import { planConsolidation } from "./consolidate.js";
21
+ import { loadTokenUsage, renderTokenUsageReport } from "./token-usage.js";
18
22
  const USAGE = `Usage:
19
23
  /evolve show this help and the current local store
20
- /evolve list [global] list entries (add "global" for the cross-session store)
24
+ /evolve list [project|global] list entries (default local staging; "project" = this project's cross-session store, "global" = cross-project store)
21
25
  /evolve history [global] show applied refinements (rollback ids)
22
26
  /evolve rollback <id> [global] deterministically revert a refinement
23
27
  /evolve plan [msg] run the LLM planner against the current store
@@ -39,7 +43,10 @@ const USAGE = `Usage:
39
43
  /evolve unmount <id> remove a hot-mounted plugin
40
44
  /evolve goal show the evolution goal (round-driven auto-review)
41
45
  /evolve goal <objective> create/update the evolution goal
42
- /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)`;
43
50
  export function registerEvolveCommand(ctx, engine, opts, runtime) {
44
51
  ctx.commands.register({
45
52
  name: "evolve",
@@ -52,8 +59,29 @@ function scopeArg(tokens) {
52
59
  if (tokens[0] === "global") {
53
60
  return { scope: "global", rest: tokens.slice(1) };
54
61
  }
62
+ if (tokens[0] === "project") {
63
+ return { scope: "project", rest: tokens.slice(1) };
64
+ }
55
65
  return { scope: "local", rest: tokens };
56
66
  }
67
+ /**
68
+ * The store id a scope reads/writes from the human command: live session id
69
+ * for local, derived project key for project, undefined for global. Throws
70
+ * for project when the session cwd is unavailable.
71
+ */
72
+ function storeIdForCommand(scope, invocation) {
73
+ if (scope === "local") {
74
+ return invocation.agent.id;
75
+ }
76
+ if (scope === "project") {
77
+ const key = projectKeyOf(invocation.agent);
78
+ if (!key) {
79
+ throw new Error("project scope needs the session cwd (unavailable here) — use local or global instead");
80
+ }
81
+ return key;
82
+ }
83
+ return undefined;
84
+ }
57
85
  /**
58
86
  * Tokenize a command's raw input with shell-like quoting:
59
87
  * - a `#` outside quotes starts a comment (rest of the line is dropped);
@@ -129,11 +157,11 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
129
157
  return success(`${USAGE}\n\n${formatHarnessStateForPrompt(engine.load("local", sessionId))}`);
130
158
  case "list": {
131
159
  const { scope } = scopeArg(rest);
132
- return success(formatHarnessStateForPrompt(engine.load(scope, sessionId)));
160
+ return success(formatHarnessStateForPrompt(engine.load(scope, storeIdForCommand(scope, invocation))));
133
161
  }
134
162
  case "history": {
135
163
  const { scope } = scopeArg(rest);
136
- const history = engine.history(scope, sessionId);
164
+ const history = engine.history(scope, storeIdForCommand(scope, invocation));
137
165
  return success(historyForPrompt(history) || "(no refinements yet)");
138
166
  }
139
167
  case "rollback": {
@@ -142,7 +170,7 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
142
170
  if (!id) {
143
171
  return error(`rollback requires a refinement id.\n${USAGE}`);
144
172
  }
145
- const result = engine.rollback(scope, sessionId, id);
173
+ const result = engine.rollback(scope, storeIdForCommand(scope, invocation), id);
146
174
  return success(renderResult(result));
147
175
  }
148
176
  case "archive":
@@ -154,9 +182,9 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
154
182
  return error(`${sub} requires an entry id.\n${USAGE}`);
155
183
  }
156
184
  if (sub === "demote") {
157
- return demoteEntry(engine, id, sessionId);
185
+ return demoteEntry(engine, id, sessionId, projectKeyOf(invocation.agent));
158
186
  }
159
- const state = engine.load(scope, sessionId);
187
+ const state = engine.load(scope, storeIdForCommand(scope, invocation));
160
188
  const found = findEntryById(state, id);
161
189
  if (!found) {
162
190
  return error(`entry ${id} not found in the ${scope} store`);
@@ -170,7 +198,7 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
170
198
  delete metadata[ARCHIVED_AT_KEY];
171
199
  }
172
200
  const archived = sub === "archive";
173
- const result = engine.apply(scope, sessionId, {
201
+ const result = engine.apply(scope, storeIdForCommand(scope, invocation), {
174
202
  summary: `${archived ? "Archive" : "Unarchive"} entry ${kind}:${id}`,
175
203
  rationale: "Human-invoked archive/unarchive via the /evolve command.",
176
204
  expectedOutcome: `Entry ${archived ? "is hidden from injection (data kept, restorable)" : "is injected again"}.`,
@@ -260,8 +288,8 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
260
288
  if (!path) {
261
289
  return error(`export requires an output path.\n${USAGE}`);
262
290
  }
263
- const state = engine.load(scope, sessionId);
264
- const history = engine.history(scope, sessionId);
291
+ const state = engine.load(scope, storeIdForCommand(scope, invocation));
292
+ const history = engine.history(scope, storeIdForCommand(scope, invocation));
265
293
  const payload = {
266
294
  version: 1,
267
295
  scope,
@@ -293,7 +321,7 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
293
321
  },
294
322
  refinements: Array.isArray(payload["refinements"]) ? payload["refinements"] : [],
295
323
  };
296
- const paths = storePaths(engine.baseDir, scope, sessionId);
324
+ const paths = storePaths(engine.baseDir, scope, storeIdForCommand(scope, invocation));
297
325
  saveHarnessState(paths.stateDir, state);
298
326
  if (Array.isArray(payload["history"])) {
299
327
  for (const result of payload["history"]) {
@@ -301,14 +329,22 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
301
329
  appendResult(paths, result);
302
330
  }
303
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
+ }
304
340
  }
305
341
  return success(`imported ${scope} store from ${path}`);
306
342
  }
307
343
  case "plan": {
308
344
  const { scope, rest: after } = scopeArg(rest);
309
345
  const instructions = after.length > 0 ? after.join(" ") : undefined;
310
- const state = engine.load(scope, sessionId);
311
- const history = engine.history(scope, sessionId);
346
+ const state = engine.load(scope, storeIdForCommand(scope, invocation));
347
+ const history = engine.history(scope, storeIdForCommand(scope, invocation));
312
348
  const proposal = await planWithLlm(ctx, {
313
349
  agent: invocation.agent,
314
350
  state,
@@ -316,13 +352,19 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
316
352
  ...(instructions ? { instructions } : {}),
317
353
  global: scope === "global",
318
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
+ },
319
361
  // skill-creator template facts (fallback: builtin guide).
320
362
  skillsRoot: join(engine.baseDir, "skills"),
321
363
  });
322
- if (scope === "global" && opts.requireGlobalApproval && proposal.edits.length > 0) {
323
- await requireGlobalApproval(ctx, invocation.agent, invocation.signal, `/evolve plan global 将应用 ${proposal.edits.length} 条编辑到跨会话 store:${proposal.summary}`);
364
+ if ((scope === "global" || scope === "project") && opts.requireGlobalApproval && proposal.edits.length > 0) {
365
+ await requireGlobalApproval(ctx, invocation.agent, invocation.signal, `/evolve plan ${scope} 将应用 ${proposal.edits.length} 条编辑到${scope === "project" ? "本项目" : "跨会话"} store:${proposal.summary}`);
324
366
  }
325
- const result = engine.apply(scope, sessionId, proposal, {
367
+ const result = engine.apply(scope, storeIdForCommand(scope, invocation), proposal, {
326
368
  scope,
327
369
  baselineState: state,
328
370
  ...(entrySourceOf(invocation.agent, sessionId) ? { source: entrySourceOf(invocation.agent, sessionId) } : {}),
@@ -332,6 +374,27 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
332
374
  case "wrapup": {
333
375
  return await executeWrapupCommand(ctx, engine, invocation, runtime.promotionPolicy);
334
376
  }
377
+ case "pause":
378
+ case "resume": {
379
+ // #21 P2 runtime switch: pause the AUTOMATIC gate only. Manual
380
+ // evolve_* tools and /evolve commands keep working — the human
381
+ // is acting explicitly there, so no gate is being bypassed.
382
+ const pausing = sub === "pause";
383
+ const current = loadGateRuntime(engine.baseDir);
384
+ if (current.paused === pausing) {
385
+ return success(`auto-review gate is already ${pausing ? "paused" : "running"} (no change).`);
386
+ }
387
+ saveGateRuntime(engine.baseDir, pausing);
388
+ return success(pausing
389
+ ? "auto-review gate paused: no automatic reviews, fate assessments, or gate LLM calls until /evolve resume. Manual evolve_* tools and /evolve commands keep working."
390
+ : "auto-review gate resumed: automatic reviews run again on their configured cadence.");
391
+ }
392
+ case "status": {
393
+ return success(renderGateStatus(engine, sessionId, projectKeyOf(invocation.agent), runtime));
394
+ }
395
+ case "usage": {
396
+ return success(renderUsageReport(engine, sessionId, projectKeyOf(invocation.agent)));
397
+ }
335
398
  case "goal": {
336
399
  return executeGoalCommand(ctx, invocation, rest);
337
400
  }
@@ -354,18 +417,23 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
354
417
  }
355
418
  /**
356
419
  * Demote (2026-08-22): hide an entry from injection WITHOUT deleting it —
357
- * the one-command remedy for global-store pollution. Searches the global
358
- * store first (the primary target: cross-project noise), then the session's
359
- * local store. The data stays; `/evolve unarchive` restores it.
420
+ * the one-command remedy for store pollution. Searches the global store
421
+ * first (the primary target: cross-project noise), then the project store,
422
+ * then the session's local store. The data stays; `/evolve unarchive` restores it.
360
423
  */
361
- function demoteEntry(engine, id, sessionId) {
362
- for (const scope of ["global", "local"]) {
363
- const state = engine.load(scope, sessionId);
424
+ function demoteEntry(engine, id, sessionId, projectKey) {
425
+ const targets = [
426
+ { scope: "global", storeId: undefined },
427
+ ...(projectKey ? [{ scope: "project", storeId: projectKey }] : []),
428
+ { scope: "local", storeId: sessionId },
429
+ ];
430
+ for (const { scope, storeId } of targets) {
431
+ const state = engine.load(scope, storeId);
364
432
  const found = findEntryById(state, id);
365
433
  if (!found)
366
434
  continue;
367
435
  const [kind, entry] = found;
368
- const result = engine.apply(scope, sessionId, {
436
+ const result = engine.apply(scope, storeId, {
369
437
  summary: `demote: archive ${kind}:${id} from the ${scope} store`,
370
438
  rationale: "Human-invoked demote via the /evolve command.",
371
439
  expectedOutcome: "The entry is hidden from injection in every scope it touched; data is kept and restorable.",
@@ -380,10 +448,105 @@ function demoteEntry(engine, id, sessionId) {
380
448
  },
381
449
  ],
382
450
  }, { scope });
383
- const restoreScope = scope === "global" ? " global" : "";
451
+ const restoreScope = scope === "global" ? " global" : scope === "project" ? " project" : "";
384
452
  return success(`demoted ${kind}:${id} from the ${scope} store (archived — restore with /evolve unarchive ${id}${restoreScope})\n${renderResult(result)}`);
385
453
  }
386
- return error(`entry ${id} not found in the global or local store`);
454
+ return error(`entry ${id} not found in the global, project, or local store`);
455
+ }
456
+ /**
457
+ * #21 status: one screen answering "is the gate on, and what is in the
458
+ * stores". The patch flag says whether the gate was registered at boot;
459
+ * the runtime switch says whether the human paused it since.
460
+ */
461
+ function renderGateStatus(engine, sessionId, projectKey, runtime) {
462
+ const paused = loadGateRuntime(engine.baseDir).paused;
463
+ const patch = runtime.autoReview === undefined ? "unknown" : runtime.autoReview ? "on" : "off";
464
+ const gateLine = patch === "off"
465
+ ? "gate: disabled in patch config (autoReview off — /evolve pause has nothing to pause)"
466
+ : `gate: ${patch === "unknown" ? "patch flag unknown" : "enabled in patch config"} · ${paused ? "PAUSED by /evolve pause (resume with /evolve resume)" : "running"}`;
467
+ const countEntries = (state) => Object.values(state.entries).reduce((n, byKind) => n + Object.keys(byKind).length, 0);
468
+ const lines = [gateLine];
469
+ lines.push(`stores: global ${countEntries(engine.load("global", undefined))} entries · local(${sessionId}) ${countEntries(engine.load("local", sessionId))} entries`);
470
+ if (projectKey) {
471
+ try {
472
+ lines[lines.length - 1] += ` · project ${countEntries(engine.load("project", projectKey))} entries`;
473
+ }
474
+ catch {
475
+ lines[lines.length - 1] += " · project (unavailable)";
476
+ }
477
+ }
478
+ const retention = engine.retention;
479
+ if (retention) {
480
+ lines.push(`retention: snapshots ${retention.snapshots} · refinements ${retention.refinements} · reviews ${retention.reviews} · token usage ${retention.tokenUsage} (historyRetain)`);
481
+ }
482
+ return lines.join("\n");
483
+ }
484
+ /**
485
+ * #21 P0 usage ledger: injection counts per entry across the stores the
486
+ * human can see (global + this session + this project). Counts are
487
+ * per-session since the v2 usage shape — "in how many sessions did this
488
+ * entry surface" — the exposure half of the #18a归零实验 verdict.
489
+ */
490
+ function renderUsageReport(engine, sessionId, projectKey) {
491
+ const store = loadUsage(engine.baseDir);
492
+ const rows = [];
493
+ const seen = new Set();
494
+ const collect = (state) => {
495
+ for (const kind of Object.keys(state.entries)) {
496
+ for (const entry of Object.values(state.entries[kind])) {
497
+ const key = `${kind}:${entry.id}`;
498
+ if (seen.has(key))
499
+ continue;
500
+ seen.add(key);
501
+ const count = getUsageCount(store, kind, entry.id);
502
+ rows.push({
503
+ key,
504
+ title: entry.title,
505
+ count,
506
+ ...(store.lastSession?.[`${kind}:${entry.id}`] ? { lastSession: store.lastSession[`${kind}:${entry.id}`] } : {}),
507
+ });
508
+ }
509
+ }
510
+ };
511
+ collect(engine.load("global", undefined));
512
+ collect(engine.load("local", sessionId));
513
+ if (projectKey) {
514
+ try {
515
+ collect(engine.load("project", projectKey));
516
+ }
517
+ catch {
518
+ // project store unavailable here — global+local still report
519
+ }
520
+ }
521
+ const liveKeys = new Set(rows.map((r) => r.key));
522
+ const orphaned = Object.keys(store.counts).filter((k) => !liveKeys.has(k)).length;
523
+ const total = rows.reduce((n, r) => n + r.count, 0);
524
+ const injected = rows.filter((r) => r.count > 0).sort((a, b) => b.count - a.count || (a.key < b.key ? -1 : 1));
525
+ const stale = rows.filter((r) => r.count === 0).sort((a, b) => (a.key < b.key ? -1 : 1));
526
+ const lines = [`injected entries: ${total} injections across ${injected.length} of ${rows.length} stored entries${orphaned > 0 ? ` (+${orphaned} historical key(s) for deleted entries)` : ""}`];
527
+ if (injected.length > 0) {
528
+ lines.push("injected (top 15):");
529
+ for (const row of injected.slice(0, 15)) {
530
+ lines.push(` ${row.key} — ${row.count}× · ${row.title}${row.lastSession ? ` (last in ${row.lastSession})` : ""}`);
531
+ }
532
+ if (injected.length > 15) {
533
+ lines.push(` … and ${injected.length - 15} more`);
534
+ }
535
+ }
536
+ else {
537
+ lines.push("injected: (none yet — nothing has surfaced into a session prompt)");
538
+ }
539
+ if (stale.length > 0) {
540
+ lines.push(`never injected (${stale.length}):`);
541
+ for (const row of stale.slice(0, 20)) {
542
+ lines.push(` ${row.key} · ${row.title}`);
543
+ }
544
+ if (stale.length > 20) {
545
+ lines.push(` … and ${stale.length - 20} more`);
546
+ }
547
+ }
548
+ lines.push("", ...renderTokenUsageReport(loadTokenUsage(engine.baseDir), engine.retention.tokenUsage));
549
+ return lines.join("\n");
387
550
  }
388
551
  function renderResult(result) {
389
552
  const applied = 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.js CHANGED
@@ -26,7 +26,7 @@ export function planLocalFates(items, candidates, globalState, policy = DEFAULT_
26
26
  splitSkipped.push({ key: item.key, reason: "not in the audited candidate list" });
27
27
  continue;
28
28
  }
29
- const blocked = splitPromoteBlocked(item, globalState, candidate.kind, policy);
29
+ const blocked = splitPromoteBlocked(item, globalState, candidate.kind, policy, candidate.metadata);
30
30
  if (blocked) {
31
31
  splitSkipped.push({ key: item.key, reason: blocked });
32
32
  continue;
@@ -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);
package/lib/index.d.ts CHANGED
@@ -18,6 +18,15 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
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,6 +83,25 @@ 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">;
@@ -87,6 +115,15 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
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
@@ -37,6 +37,15 @@ export const Config = z.object({
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,12 +134,12 @@ 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",
118
141
  order: config.sectionOrder ?? 118,
119
- text: "You have a continual harness: versioned, persistent prompt notes, memories, skills, and subagent specs. Prompt notes and delegation specs are injected below; use evolve_list for the full state. Create an entry (evolve_add) after a repeated failure, a reusable tactic, a durable fact or preference, a repeated procedure, or a repeated delegation role. Keep edits small and evidence-backed; prefer local scope, use global: true only for stable cross-session lessons. Update or delete (evolve_update / evolve_delete) when an entry is wrong or obsolete; roll back faulty refinements with evolve_rollback. Every edit is snapshotted, versioned, and recorded — no edit can be silently lost.",
142
+ text: "You have a continual harness: versioned, persistent prompt notes, memories, skills, and subagent specs. Memories are ONE FACT per entry, typed user/feedback/project/reference (feedback = pitfall + Why + How to apply). Stores: local (this session's staging), project (this project's cross-session store), global (cross-project). Prompt notes and delegation specs are injected below; use evolve_list for the full state. Create an entry (evolve_add) after a repeated failure, a reusable tactic, a durable fact or preference, a repeated procedure, or a repeated delegation role. Keep edits small and evidence-backed; prefer local scope, use project for repo-specific lessons, global: true only for stable cross-project lessons. Update or delete (evolve_update / evolve_delete) when an entry is wrong or obsolete; roll back faulty refinements with evolve_rollback. Every edit is snapshotted, versioned, and recorded — no edit can be silently lost.",
120
143
  });
121
144
  // Phase 2: make prompt entries real system-prompt content and subagent
122
145
  // entries real delegation specs. The text is a provider evaluated at every
@@ -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 —
@@ -165,7 +189,10 @@ export function apply(ctx, config) {
165
189
  promotionPolicy,
166
190
  autoCase: config.autoCase ?? true,
167
191
  rubricKey,
192
+ ...(config.historyRetain?.reviews !== undefined ? { reviewsRetain: config.historyRetain.reviews } : {}),
168
193
  ...(config.reviewModel ? { reviewModel: config.reviewModel } : {}),
194
+ ...(config.plannerPrefixCache ? { prefixCacheMode: config.plannerPrefixCache } : {}),
195
+ ...(config.plannerPrefixMaxChars !== undefined ? { prefixMaxChars: config.plannerPrefixMaxChars } : {}),
169
196
  });
170
197
  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
198
  }