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/README.md +19 -11
- package/README.zh.md +19 -11
- package/lib/apply.js +17 -9
- package/lib/auto.d.ts +19 -4
- package/lib/auto.js +61 -8
- package/lib/command.d.ts +6 -0
- package/lib/command.js +190 -27
- package/lib/evolve-event.d.ts +4 -2
- package/lib/evolve-event.js +12 -2
- package/lib/fate.js +8 -2
- package/lib/index.d.ts +56 -0
- package/lib/index.js +29 -2
- package/lib/inject.d.ts +35 -9
- package/lib/inject.js +86 -36
- package/lib/llm-text.d.ts +23 -0
- package/lib/llm-text.js +50 -33
- package/lib/planner.d.ts +13 -1
- package/lib/planner.js +31 -7
- package/lib/prefix-cache.d.ts +78 -0
- package/lib/prefix-cache.js +168 -0
- package/lib/project.d.ts +28 -0
- package/lib/project.js +66 -0
- package/lib/review.d.ts +11 -0
- package/lib/review.js +23 -2
- package/lib/runtime.d.ts +24 -0
- package/lib/runtime.js +83 -0
- package/lib/service.d.ts +11 -1
- package/lib/service.js +24 -3
- package/lib/state.d.ts +8 -4
- package/lib/state.js +14 -7
- package/lib/store.d.ts +44 -0
- package/lib/store.js +99 -4
- package/lib/token-usage.d.ts +68 -0
- package/lib/token-usage.js +211 -0
- package/lib/tool.d.ts +13 -1
- package/lib/tool.js +118 -32
- package/lib/types.d.ts +14 -2
- package/lib/types.js +12 -0
- package/lib/validate.js +30 -0
- package/lib/wrapup-command.js +11 -2
- package/lib/wrapup.d.ts +7 -2
- package/lib/wrapup.js +21 -1
- package/package.json +10 -10
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 {
|
|
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 (
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
264
|
-
const history = engine.history(scope,
|
|
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,
|
|
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,
|
|
311
|
-
const history = engine.history(scope,
|
|
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
|
|
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,
|
|
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
|
|
358
|
-
*
|
|
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
|
-
|
|
363
|
-
|
|
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,
|
|
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);
|
package/lib/evolve-event.d.ts
CHANGED
|
@@ -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
|
package/lib/evolve-event.js
CHANGED
|
@@ -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-
|
|
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
|
}
|