dsh-continual-evolve 0.7.2 → 0.7.4
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 +16 -14
- package/README.zh.md +16 -14
- package/lib/approval.d.ts +11 -4
- package/lib/approval.js +23 -7
- package/lib/auto.d.ts +24 -0
- package/lib/auto.js +143 -14
- package/lib/index.js +6 -4
- package/lib/llm-text.d.ts +56 -5
- package/lib/llm-text.js +100 -27
- package/lib/memory-agent.d.ts +113 -0
- package/lib/memory-agent.js +536 -0
- package/lib/planner.js +2 -1
- package/lib/review-scheduler.d.ts +9 -8
- package/lib/review-scheduler.js +118 -36
- package/lib/review.d.ts +1 -1
- package/lib/review.js +8 -6
- package/lib/service.d.ts +9 -0
- package/lib/service.js +46 -20
- package/lib/token-usage.d.ts +1 -1
- package/lib/token-usage.js +8 -8
- package/lib/turn-snapshot.d.ts +10 -0
- package/lib/turn-snapshot.js +34 -0
- package/lib/wrapup.js +2 -1
- package/package.json +1 -1
package/lib/review-scheduler.js
CHANGED
|
@@ -1,90 +1,172 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Serial latest-
|
|
2
|
+
* Serial latest-boundary scheduler for automatic memory+review snapshots.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* next snapshot retries the same durable boundary.
|
|
4
|
+
* One runner exists per session. A burst is coalesced by durable cursor rather
|
|
5
|
+
* than promise arrival: once multiple pending captures settle, the highest
|
|
6
|
+
* comparable boundary runs first. A cursor advances only after success/no-op;
|
|
7
|
+
* errors, aborts, and stale boundaries leave it unchanged.
|
|
9
8
|
*/
|
|
9
|
+
/** Compare two scheduler cursors; undefined means the cursor families differ. */
|
|
10
|
+
export function compareReviewCursors(left, right) {
|
|
11
|
+
if (left === right)
|
|
12
|
+
return 0;
|
|
13
|
+
const leftMatch = /^(seq|index):(\d+)$/.exec(left);
|
|
14
|
+
const rightMatch = /^(seq|index):(\d+)$/.exec(right);
|
|
15
|
+
if (!leftMatch || !rightMatch || leftMatch[1] !== rightMatch[1])
|
|
16
|
+
return undefined;
|
|
17
|
+
return Number(leftMatch[2]) - Number(rightMatch[2]);
|
|
18
|
+
}
|
|
19
|
+
/** Bound unresolved acquisitions; actual auto capture is serialized per session. */
|
|
20
|
+
const MAX_PENDING_ACQUISITIONS = 16;
|
|
10
21
|
/**
|
|
11
|
-
* Create a scheduler with ZCode-compatible coalescing
|
|
22
|
+
* Create a scheduler with ZCode-compatible serial/coalescing semantics.
|
|
12
23
|
*
|
|
13
24
|
* @param execute - one serial extraction/review operation
|
|
14
25
|
* @param cursorOf - extracts the durable boundary from a snapshot
|
|
15
26
|
*/
|
|
16
27
|
export function createReviewScheduler(execute, cursorOf) {
|
|
17
28
|
let cursor;
|
|
18
|
-
let latestPending;
|
|
19
29
|
let running;
|
|
20
30
|
let shuttingDown = false;
|
|
31
|
+
let wakePending;
|
|
32
|
+
const pending = [];
|
|
21
33
|
const shutdownController = new AbortController();
|
|
34
|
+
const signal = shutdownController.signal;
|
|
35
|
+
const trackPending = (acquisition) => {
|
|
36
|
+
const entry = {};
|
|
37
|
+
entry.promise = acquisition.then((result) => {
|
|
38
|
+
entry.result = result;
|
|
39
|
+
wakePending?.();
|
|
40
|
+
return result;
|
|
41
|
+
});
|
|
42
|
+
pending.push(entry);
|
|
43
|
+
while (pending.length > MAX_PENDING_ACQUISITIONS)
|
|
44
|
+
pending.shift();
|
|
45
|
+
};
|
|
46
|
+
const takeHighestPending = async () => {
|
|
47
|
+
while (!signal.aborted) {
|
|
48
|
+
for (let index = pending.length - 1; index >= 0; index -= 1) {
|
|
49
|
+
if (pending[index]?.result?.status === "error")
|
|
50
|
+
pending.splice(index, 1);
|
|
51
|
+
}
|
|
52
|
+
if (pending.length === 0)
|
|
53
|
+
return undefined;
|
|
54
|
+
const settled = pending.filter((entry) => entry.result !== undefined);
|
|
55
|
+
if (settled.length > 0) {
|
|
56
|
+
let bestIndex = -1;
|
|
57
|
+
let bestCursor;
|
|
58
|
+
for (let index = 0; index < settled.length; index += 1) {
|
|
59
|
+
const entry = settled[index];
|
|
60
|
+
if (!entry || entry.result?.status !== "acquired")
|
|
61
|
+
continue;
|
|
62
|
+
const candidate = cursorOf(entry.result.snapshot);
|
|
63
|
+
const order = bestCursor === undefined || candidate === undefined
|
|
64
|
+
? 1
|
|
65
|
+
: compareReviewCursors(candidate, bestCursor);
|
|
66
|
+
if (bestIndex < 0 || order === undefined || order > 0) {
|
|
67
|
+
bestIndex = pending.indexOf(entry);
|
|
68
|
+
bestCursor = candidate;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (bestIndex < 0) {
|
|
72
|
+
pending.length = 0;
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
const chosen = pending[bestIndex];
|
|
76
|
+
pending.splice(bestIndex, 1);
|
|
77
|
+
for (let index = pending.length - 1; index >= 0; index -= 1) {
|
|
78
|
+
if (pending[index]?.result !== undefined)
|
|
79
|
+
pending.splice(index, 1);
|
|
80
|
+
}
|
|
81
|
+
return chosen?.result;
|
|
82
|
+
}
|
|
83
|
+
await new Promise((resolve) => {
|
|
84
|
+
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
85
|
+
const onAbort = () => {
|
|
86
|
+
cleanup();
|
|
87
|
+
resolve();
|
|
88
|
+
};
|
|
89
|
+
wakePending = () => {
|
|
90
|
+
cleanup();
|
|
91
|
+
wakePending = undefined;
|
|
92
|
+
resolve();
|
|
93
|
+
};
|
|
94
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return undefined;
|
|
98
|
+
};
|
|
22
99
|
const processSnapshot = async (snapshot) => {
|
|
23
|
-
if (shuttingDown ||
|
|
100
|
+
if (shuttingDown || signal.aborted)
|
|
24
101
|
return;
|
|
102
|
+
const nextCursor = cursorOf(snapshot);
|
|
103
|
+
if (cursor !== undefined && nextCursor !== undefined) {
|
|
104
|
+
const order = compareReviewCursors(nextCursor, cursor);
|
|
105
|
+
if (order !== undefined && order <= 0)
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
25
108
|
let status;
|
|
26
109
|
try {
|
|
27
|
-
status = await execute({ snapshot, signal
|
|
110
|
+
status = await execute({ snapshot, signal });
|
|
28
111
|
}
|
|
29
112
|
catch {
|
|
30
|
-
// A failed acquisition/extraction is deliberately contained. The
|
|
31
|
-
// cursor remains unchanged and the next snapshot retries it.
|
|
32
113
|
return;
|
|
33
114
|
}
|
|
34
|
-
if (!shuttingDown && (status === "success" || status === "no-op")) {
|
|
35
|
-
|
|
115
|
+
if (!shuttingDown && (status === "success" || status === "no-op") && nextCursor !== undefined) {
|
|
116
|
+
const order = cursor === undefined ? 1 : compareReviewCursors(nextCursor, cursor);
|
|
117
|
+
if (order === undefined || order > 0)
|
|
118
|
+
cursor = nextCursor;
|
|
36
119
|
}
|
|
37
120
|
};
|
|
38
121
|
const run = async (first) => {
|
|
39
122
|
try {
|
|
40
|
-
let
|
|
41
|
-
while (
|
|
42
|
-
|
|
43
|
-
if (acquisition.status === "shutdown" || shuttingDown)
|
|
44
|
-
break;
|
|
45
|
-
if (acquisition.status === "acquired") {
|
|
123
|
+
let acquisition = await waitForSnapshotAcquisitionOrShutdown(first, signal);
|
|
124
|
+
while (acquisition.status !== "shutdown" && !shuttingDown) {
|
|
125
|
+
if (acquisition.status === "acquired")
|
|
46
126
|
await processSnapshot(acquisition.snapshot);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
127
|
+
const next = await takeHighestPending();
|
|
128
|
+
if (!next)
|
|
129
|
+
break;
|
|
130
|
+
acquisition = next;
|
|
50
131
|
}
|
|
51
132
|
}
|
|
52
133
|
finally {
|
|
53
|
-
if (shuttingDown)
|
|
54
|
-
|
|
134
|
+
if (shuttingDown) {
|
|
135
|
+
pending.length = 0;
|
|
136
|
+
wakePending?.();
|
|
137
|
+
wakePending = undefined;
|
|
138
|
+
}
|
|
55
139
|
running = undefined;
|
|
56
140
|
}
|
|
57
141
|
};
|
|
58
142
|
return {
|
|
59
143
|
async drain() {
|
|
60
|
-
while (running)
|
|
144
|
+
while (running)
|
|
61
145
|
await running;
|
|
62
|
-
}
|
|
63
146
|
},
|
|
64
147
|
getCursor() {
|
|
65
148
|
return cursor;
|
|
66
149
|
},
|
|
67
150
|
hasPendingWork() {
|
|
68
|
-
return running !== undefined ||
|
|
151
|
+
return running !== undefined || pending.length > 0;
|
|
69
152
|
},
|
|
70
153
|
schedule(snapshot) {
|
|
71
154
|
if (shuttingDown)
|
|
72
155
|
return;
|
|
73
156
|
const acquisition = acquireSnapshot(snapshot);
|
|
74
|
-
if (running)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
running = run(acquisition);
|
|
157
|
+
if (running)
|
|
158
|
+
trackPending(acquisition);
|
|
159
|
+
else
|
|
160
|
+
running = run(acquisition);
|
|
81
161
|
},
|
|
82
162
|
shutdown() {
|
|
83
163
|
if (shuttingDown)
|
|
84
164
|
return;
|
|
85
165
|
shuttingDown = true;
|
|
86
|
-
|
|
166
|
+
pending.length = 0;
|
|
87
167
|
shutdownController.abort();
|
|
168
|
+
wakePending?.();
|
|
169
|
+
wakePending = undefined;
|
|
88
170
|
},
|
|
89
171
|
};
|
|
90
172
|
}
|
package/lib/review.d.ts
CHANGED
|
@@ -44,7 +44,7 @@ export interface ReviewOptions {
|
|
|
44
44
|
/** Direct-call token ledger destination; the review phase is fixed by this module. */
|
|
45
45
|
tokenUsage?: TokenUsageTarget;
|
|
46
46
|
}
|
|
47
|
-
export declare const AUTO_REVIEW_SYSTEM_PROMPT = "You are the automatic /evolve review gate.\n\nDecide whether this checkpoint should
|
|
47
|
+
export declare const AUTO_REVIEW_SYSTEM_PROMPT = "You are the automatic /evolve review gate.\n\nDecide whether this checkpoint should continue the non-memory /evolve path.\nA dedicated memory extraction agent has already processed the same snapshot\nwith a frozen memory manifest, so do NOT approve solely to create, update,\narchive, or delete a memory entry. Decline with a rationale that the memory\nphase owns that evidence. For the remaining kinds, approve repeated failures,\nreusable tactics, repeated delegation roles, or narrow behavioral policies.\n\nThe current harness state below includes GLOBAL entries (scope=global) plus\nthis session's local entries (scope=local). When a topic is already covered\nby a global entry, do NOT approve a local duplicate of it \u2014 decline and say\nin the rationale that the topic is already covered globally.\n\nReject one-off noise, unsupported hypotheses, transient tool outputs, and\nrequests that carry no reusable content.\n\nStale local entries (superseded, long-unused, obsolete facts) are a valid\nrefine target: approve with instructions naming the entry ids, and tell the\nplanner to archive them (archive hides from injection, data stays restorable)\nrather than delete.\n\nSkill-related trajectories (the evidence concerns creating or improving a\nskill entry) are judged against the DSH skill quality standard (skill-audit\ndimensions: frontmatter routing, the 7 structural features, paragraph\nskeleton, no duplication of the official 11 skills or covered skills).\nApprove only when the trajectory shows a REAL trigger scenario and the\nresulting skill would meet the standard; otherwise decline and say in the\nrationale what must improve \u2014 drafting follows skill-creator, and the\nplanner receives the standard as its <skill_quality_standard> block.\n\nRepeated multi-step workflows (session start/end routines, recurring\nwrap-up or handoff procedures) are a valid refine target: approve with\ninstructions telling the planner to propose a guidance skill (kind=skill,\nskill_kind=guidance \u2014 a SKILL.md document, no python reference). Only\npropose when the same workflow recurs in the trajectory \u2014 never for\none-off flows. Auto-created skills are always offered to the user for a\ndecision before they land; the gate never writes a skill silently.\n\nReturn JSON only:\n{\n \"shouldRefine\": true|false,\n \"rationale\": \"short reason\",\n \"instructions\": \"optional concise instructions for /evolve if shouldRefine is true\"\n}";
|
|
48
48
|
/** Parse the gate's JSON reply. */
|
|
49
49
|
export declare function parseAutoRefineReview(text: string): AutoRefineReview;
|
|
50
50
|
/** Serialize surface events to bounded role-prefixed text. */
|
package/lib/review.js
CHANGED
|
@@ -2,15 +2,16 @@ import { extractJsonObject } from "./plan.js";
|
|
|
2
2
|
import { formatHarnessStateForPrompt, historyForPrompt } from "./render.js";
|
|
3
3
|
import { sessionEventsOf } from "./inject.js";
|
|
4
4
|
import { buildPrefixMessages, detectPlannerRoute, resolvePrefixCache } from "./prefix-cache.js";
|
|
5
|
-
import { streamText } from "./llm-text.js";
|
|
5
|
+
import { asLlmSessionId, streamText } from "./llm-text.js";
|
|
6
6
|
import { createTokenUsageObserver } from "./token-usage.js";
|
|
7
7
|
export const AUTO_REVIEW_SYSTEM_PROMPT = `You are the automatic /evolve review gate.
|
|
8
8
|
|
|
9
|
-
Decide whether this checkpoint should
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
that
|
|
9
|
+
Decide whether this checkpoint should continue the non-memory /evolve path.
|
|
10
|
+
A dedicated memory extraction agent has already processed the same snapshot
|
|
11
|
+
with a frozen memory manifest, so do NOT approve solely to create, update,
|
|
12
|
+
archive, or delete a memory entry. Decline with a rationale that the memory
|
|
13
|
+
phase owns that evidence. For the remaining kinds, approve repeated failures,
|
|
14
|
+
reusable tactics, repeated delegation roles, or narrow behavioral policies.
|
|
14
15
|
|
|
15
16
|
The current harness state below includes GLOBAL entries (scope=global) plus
|
|
16
17
|
this session's local entries (scope=local). When a topic is already covered
|
|
@@ -125,6 +126,7 @@ export async function reviewAutoRefine(ctx, options) {
|
|
|
125
126
|
const text = await streamText(ctx, {
|
|
126
127
|
provider,
|
|
127
128
|
model,
|
|
129
|
+
sessionId: asLlmSessionId(agent.id),
|
|
128
130
|
system: AUTO_REVIEW_SYSTEM_PROMPT,
|
|
129
131
|
prompt: userPrompt,
|
|
130
132
|
maxTokens: options.budgetTokens ?? 8000,
|
package/lib/service.d.ts
CHANGED
|
@@ -6,6 +6,14 @@
|
|
|
6
6
|
import type { EntrySource, HarnessScope, RefinementProposal, RefinementResult } from "./types.js";
|
|
7
7
|
import { applyRefinementProposal } from "./apply.js";
|
|
8
8
|
import type { HistoryRetention } from "./store.js";
|
|
9
|
+
/** A post-commit apply failure that still carries the durable applied result. */
|
|
10
|
+
export declare class EvolutionApplyPostCommitError extends Error {
|
|
11
|
+
readonly result: RefinementResult;
|
|
12
|
+
readonly scope: HarnessScope;
|
|
13
|
+
readonly storeId: string | undefined;
|
|
14
|
+
readonly cause: unknown;
|
|
15
|
+
constructor(message: string, result: RefinementResult, scope: HarnessScope, storeId: string | undefined, cause: unknown);
|
|
16
|
+
}
|
|
9
17
|
export interface ApplyContext {
|
|
10
18
|
scope: HarnessScope;
|
|
11
19
|
sessionId?: string;
|
|
@@ -32,6 +40,7 @@ export declare function createEvolutionEngine(baseDir: string, hooks?: Evolution
|
|
|
32
40
|
load: (scope: HarnessScope, sessionId: string | undefined) => import("./types.js").HarnessState;
|
|
33
41
|
apply: (scope: HarnessScope, sessionId: string | undefined, proposal: RefinementProposal, context?: ApplyContext) => RefinementResult;
|
|
34
42
|
rollback: (scope: HarnessScope, sessionId: string | undefined, refinementId: string) => RefinementResult;
|
|
43
|
+
rollbackResult: (scope: HarnessScope, sessionId: string | undefined, target: RefinementResult) => RefinementResult;
|
|
35
44
|
history: (scope: HarnessScope, sessionId: string | undefined) => RefinementResult[];
|
|
36
45
|
baseDir: string;
|
|
37
46
|
retention: HistoryRetention;
|
package/lib/service.js
CHANGED
|
@@ -5,6 +5,20 @@ import { rollbackProposal } from "./rollback.js";
|
|
|
5
5
|
import { loadHarnessState, saveHarnessState } from "./state.js";
|
|
6
6
|
import { appendResult, loadResults, pruneJsonlFile, pruneSnapshots, resolveHistoryRetention, snapshotBefore, storePaths } from "./store.js";
|
|
7
7
|
import { CONFLICT_BLOCK_SCORE, CONFLICT_WARN_SCORE, buildConflictNotice, mostSimilarEntry, secretLeakReason } from "./promotion.js";
|
|
8
|
+
/** A post-commit apply failure that still carries the durable applied result. */
|
|
9
|
+
export class EvolutionApplyPostCommitError extends Error {
|
|
10
|
+
result;
|
|
11
|
+
scope;
|
|
12
|
+
storeId;
|
|
13
|
+
cause;
|
|
14
|
+
constructor(message, result, scope, storeId, cause) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.result = result;
|
|
17
|
+
this.scope = scope;
|
|
18
|
+
this.storeId = storeId;
|
|
19
|
+
this.cause = cause;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
8
22
|
export function createEvolutionEngine(baseDir, hooks = {}, opts = {}) {
|
|
9
23
|
const retention = resolveHistoryRetention(opts.historyRetain);
|
|
10
24
|
/**
|
|
@@ -26,27 +40,29 @@ export function createEvolutionEngine(baseDir, hooks = {}, opts = {}) {
|
|
|
26
40
|
// 2026-08-28 B1: the two roles were previously folded into one object,
|
|
27
41
|
// which made the advertised guard unreachable).
|
|
28
42
|
const state = load(scope, sessionId);
|
|
29
|
-
// Write-time conflict guard (R2):
|
|
30
|
-
// the existing same-kind entries BEFORE any side
|
|
31
|
-
// near-duplicate is rejected with an actionable error
|
|
32
|
-
// instead), a moderate overlap proceeds stamped with
|
|
43
|
+
// Write-time conflict guard (R2): persistent-scope creates are
|
|
44
|
+
// checked against the existing same-kind entries BEFORE any side
|
|
45
|
+
// effect — a near-duplicate is rejected with an actionable error
|
|
46
|
+
// (evolve_update instead), a moderate overlap proceeds stamped with
|
|
33
47
|
// CONFLICT_HINT_KEY. Rollbacks bypass the guard: re-creating an entry
|
|
34
48
|
// that resembles its successor is the point of rollback. Local scope
|
|
35
49
|
// is never blocked (scratch space); the wrapup/fate promotion path
|
|
36
50
|
// already enforces its own overlap policy there.
|
|
37
51
|
//
|
|
38
|
-
// Secret-leak guard (P0, same throat):
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
// can plant: title, content, and the JSON forms
|
|
43
|
-
// arguments, and metadata
|
|
44
|
-
//
|
|
52
|
+
// Secret-leak guard (P0, same throat): every persistent-scope create
|
|
53
|
+
// and update is screened before any side effect. Project and global
|
|
54
|
+
// stores both outlive the current session, so approval never turns a
|
|
55
|
+
// credential-shaped literal into an allowed write. The screen covers
|
|
56
|
+
// every field the edit can plant: title, content, and the JSON forms
|
|
57
|
+
// of reference, arguments, and metadata. Fixed patterns, not policy-
|
|
58
|
+
// configurable.
|
|
45
59
|
const warnHits = new Map();
|
|
46
|
-
if (scope === "global" && !context?.rollbackOf) {
|
|
60
|
+
if ((scope === "project" || scope === "global") && !context?.rollbackOf) {
|
|
47
61
|
for (const [index, edit] of proposal.edits.entries()) {
|
|
48
62
|
if (edit.action === "create" || edit.action === "update") {
|
|
49
63
|
const screenable = [
|
|
64
|
+
typeof edit.id === "string" ? edit.id : "",
|
|
65
|
+
typeof edit.path === "string" ? edit.path : "",
|
|
50
66
|
typeof edit.title === "string" ? edit.title : "",
|
|
51
67
|
typeof edit.content === "string" ? edit.content : "",
|
|
52
68
|
edit.reference !== undefined ? JSON.stringify(edit.reference) : "",
|
|
@@ -100,7 +116,12 @@ export function createEvolutionEngine(baseDir, hooks = {}, opts = {}) {
|
|
|
100
116
|
}
|
|
101
117
|
}
|
|
102
118
|
saveHarnessState(paths.stateDir, state);
|
|
103
|
-
|
|
119
|
+
try {
|
|
120
|
+
appendResult(paths, result);
|
|
121
|
+
}
|
|
122
|
+
catch (cause) {
|
|
123
|
+
throw new EvolutionApplyPostCommitError(`Refinement ${result.id} persisted state but failed to append history: ${cause instanceof Error ? cause.message : String(cause)}`, result, scope, sessionId, cause);
|
|
124
|
+
}
|
|
104
125
|
// Storage hygiene (#20): bound the append-only past at write time —
|
|
105
126
|
// snapshots keep the newest N, the per-store history keeps its tail.
|
|
106
127
|
// Best-effort (never throws): a prune failure must not fail the apply.
|
|
@@ -116,9 +137,18 @@ export function createEvolutionEngine(baseDir, hooks = {}, opts = {}) {
|
|
|
116
137
|
catch {
|
|
117
138
|
// ignored — the next apply retries
|
|
118
139
|
}
|
|
119
|
-
|
|
140
|
+
try {
|
|
141
|
+
hooks.onApplied?.(result);
|
|
142
|
+
}
|
|
143
|
+
catch (cause) {
|
|
144
|
+
throw new EvolutionApplyPostCommitError(`Refinement ${result.id} persisted state but a post-commit hook failed: ${cause instanceof Error ? cause.message : String(cause)}`, result, scope, sessionId, cause);
|
|
145
|
+
}
|
|
120
146
|
return result;
|
|
121
147
|
}
|
|
148
|
+
/** Roll back an already-observed result without consulting history. */
|
|
149
|
+
function rollbackResult(scope, sessionId, target) {
|
|
150
|
+
return apply(scope, sessionId, rollbackProposal(target), { scope, rollbackOf: target.id });
|
|
151
|
+
}
|
|
122
152
|
function rollback(scope, sessionId, refinementId) {
|
|
123
153
|
const paths = storePaths(baseDir, scope, sessionId);
|
|
124
154
|
const history = loadResults(paths);
|
|
@@ -126,15 +156,11 @@ export function createEvolutionEngine(baseDir, hooks = {}, opts = {}) {
|
|
|
126
156
|
if (!target) {
|
|
127
157
|
throw new Error(`Refinement ${refinementId} not found in ${scope} history`);
|
|
128
158
|
}
|
|
129
|
-
|
|
130
|
-
// The rollback refinement carries rollbackOf so the audit chain links
|
|
131
|
-
// the inverse operation back to its origin (previously the rollback
|
|
132
|
-
// record only echoed "Rollback refinement <id>" in its summary text).
|
|
133
|
-
return apply(scope, sessionId, proposal, { scope, rollbackOf: refinementId });
|
|
159
|
+
return rollbackResult(scope, sessionId, target);
|
|
134
160
|
}
|
|
135
161
|
function history(scope, sessionId) {
|
|
136
162
|
return loadResults(storePaths(baseDir, scope, sessionId));
|
|
137
163
|
}
|
|
138
|
-
return { load, apply, rollback, history, baseDir, retention };
|
|
164
|
+
return { load, apply, rollback, rollbackResult, history, baseDir, retention };
|
|
139
165
|
}
|
|
140
166
|
//# sourceMappingURL=service.js.map
|
package/lib/token-usage.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { TokenUsage } from "@deepseek-ai/dsh-llm";
|
|
|
2
2
|
import type { StreamTextObservation, StreamTextUsageObserver } from "./llm-text.js";
|
|
3
3
|
export declare const TOKEN_USAGE_LEDGER_VERSION = 1;
|
|
4
4
|
/** Plugin-owned direct call paths represented in the ledger. */
|
|
5
|
-
export declare const TOKEN_USAGE_PHASES: readonly ["review", "planner", "wrapup", "fate"];
|
|
5
|
+
export declare const TOKEN_USAGE_PHASES: readonly ["review", "memory", "planner", "wrapup", "fate"];
|
|
6
6
|
export type TokenUsagePhase = (typeof TOKEN_USAGE_PHASES)[number];
|
|
7
7
|
/** Whether the provider supplied a usage sample; missing is never treated as zero. */
|
|
8
8
|
export type TokenUsageStatus = "reported" | "missing";
|
package/lib/token-usage.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Provider-reported token accounting for plugin-owned direct LLM calls.
|
|
3
3
|
*
|
|
4
|
-
* The review gate, planner, wrap-up command, and
|
|
5
|
-
* the shared
|
|
6
|
-
* (they pass no sessionId), so the host session
|
|
7
|
-
* them. This ledger records the exact
|
|
8
|
-
* the one shared stream boundary
|
|
4
|
+
* The review gate, planner, dedicated memory agent, wrap-up command, and
|
|
5
|
+
* local-fate classifier all use the shared model-call boundary. These calls do
|
|
6
|
+
* not belong to an Agent turn (they pass no sessionId), so the host session
|
|
7
|
+
* token-meter cannot attribute them. This ledger records the exact
|
|
8
|
+
* `TokenUsage` emitted by the adapter at the one shared stream boundary.
|
|
9
9
|
*
|
|
10
10
|
* Storage: `<baseDir>/evolve/token-usage.jsonl`. The file contains route and
|
|
11
11
|
* count metadata only — never prompts, responses, or tool arguments — and is
|
|
@@ -17,7 +17,7 @@ import { EVOLVE_DIR, pruneJsonlFile } from "./store.js";
|
|
|
17
17
|
const TOKEN_USAGE_FILE = "token-usage.jsonl";
|
|
18
18
|
export const TOKEN_USAGE_LEDGER_VERSION = 1;
|
|
19
19
|
/** Plugin-owned direct call paths represented in the ledger. */
|
|
20
|
-
export const TOKEN_USAGE_PHASES = ["review", "planner", "wrapup", "fate"];
|
|
20
|
+
export const TOKEN_USAGE_PHASES = ["review", "memory", "planner", "wrapup", "fate"];
|
|
21
21
|
/** Absolute path of the direct-call token ledger. */
|
|
22
22
|
export function tokenUsagePath(baseDir) {
|
|
23
23
|
return join(baseDir, EVOLVE_DIR, TOKEN_USAGE_FILE);
|
|
@@ -139,7 +139,7 @@ export function renderTokenUsageReport(ledger, retain) {
|
|
|
139
139
|
return [
|
|
140
140
|
`direct LLM token usage: no valid calls in the retained tail.${corrupt}`,
|
|
141
141
|
`window: last ${retain} call(s), not a lifetime total.`,
|
|
142
|
-
"scope: review/planner/wrapup/fate only; host benchmark subagents, their agent-loop calls, and per-entry injection cost are excluded.",
|
|
142
|
+
"scope: review/memory/planner/wrapup/fate only; host benchmark subagents, their agent-loop calls, and per-entry injection cost are excluded.",
|
|
143
143
|
];
|
|
144
144
|
}
|
|
145
145
|
const totals = aggregateTokenUsage(records);
|
|
@@ -166,7 +166,7 @@ export function renderTokenUsageReport(ledger, retain) {
|
|
|
166
166
|
const lastTotal = last.usage ? exactTokenTotal(last.usage) : "usage unavailable";
|
|
167
167
|
lines.push(`last: ${last.phase} ${last.provider}/${last.model} · ${last.outcome} · ${lastTotal} tokens · ${last.timestamp}`);
|
|
168
168
|
}
|
|
169
|
-
lines.push("scope: review/planner/wrapup/fate only; host benchmark subagents, their agent-loop calls, and per-entry injection cost are excluded.");
|
|
169
|
+
lines.push("scope: review/memory/planner/wrapup/fate only; host benchmark subagents, their agent-loop calls, and per-entry injection cost are excluded.");
|
|
170
170
|
return lines;
|
|
171
171
|
}
|
|
172
172
|
function isTokenUsageRecord(value) {
|
package/lib/turn-snapshot.d.ts
CHANGED
|
@@ -23,6 +23,9 @@ export interface TurnSnapshot {
|
|
|
23
23
|
readonly trajectory: string;
|
|
24
24
|
readonly userText: string;
|
|
25
25
|
readonly sourceSeqs: readonly number[];
|
|
26
|
+
readonly maxChars: number;
|
|
27
|
+
/** Cursor supplied by the shared scheduler before this snapshot selected events. */
|
|
28
|
+
readonly capturedAfterCursor?: string;
|
|
26
29
|
readonly eligible: boolean;
|
|
27
30
|
readonly skipReason?: SnapshotSkipReason;
|
|
28
31
|
}
|
|
@@ -43,6 +46,13 @@ export declare function captureTurnSnapshot(ctx: Context, agent: Agent, opts: {
|
|
|
43
46
|
cursor?: string;
|
|
44
47
|
maxChars: number;
|
|
45
48
|
}): Promise<TurnSnapshot>;
|
|
49
|
+
/**
|
|
50
|
+
* Rebase one captured snapshot onto a phase-specific cursor while preserving
|
|
51
|
+
* its outer boundary. The memory phase uses this after a downstream review or
|
|
52
|
+
* fate failure: already-processed evidence is removed, but newly captured rows
|
|
53
|
+
* remain available without moving the shared scheduler cursor backwards.
|
|
54
|
+
*/
|
|
55
|
+
export declare function sliceTurnSnapshot(snapshot: TurnSnapshot, cursor: string): TurnSnapshot;
|
|
46
56
|
/**
|
|
47
57
|
* Decide whether the incremental rows deserve a model call. This intentionally
|
|
48
58
|
* does not decide whether a fact is valuable; that remains the review model's
|
package/lib/turn-snapshot.js
CHANGED
|
@@ -21,6 +21,27 @@ export async function captureTurnSnapshot(ctx, agent, opts) {
|
|
|
21
21
|
trajectory: eligibility.eligible ? serializeSnapshotEvents(events, opts.maxChars) : "",
|
|
22
22
|
userText: eligibility.userText,
|
|
23
23
|
sourceSeqs: directUserSeqs(events),
|
|
24
|
+
maxChars: opts.maxChars,
|
|
25
|
+
...(opts.cursor === undefined ? {} : { capturedAfterCursor: opts.cursor }),
|
|
26
|
+
eligible: eligibility.eligible,
|
|
27
|
+
...(eligibility.reason ? { skipReason: eligibility.reason } : {}),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Rebase one captured snapshot onto a phase-specific cursor while preserving
|
|
32
|
+
* its outer boundary. The memory phase uses this after a downstream review or
|
|
33
|
+
* fate failure: already-processed evidence is removed, but newly captured rows
|
|
34
|
+
* remain available without moving the shared scheduler cursor backwards.
|
|
35
|
+
*/
|
|
36
|
+
export function sliceTurnSnapshot(snapshot, cursor) {
|
|
37
|
+
const events = sliceSnapshotEvents(snapshot, cursor);
|
|
38
|
+
const eligibility = evaluateSnapshotEligibility(events, false);
|
|
39
|
+
return {
|
|
40
|
+
...snapshot,
|
|
41
|
+
events,
|
|
42
|
+
trajectory: eligibility.eligible ? serializeSnapshotEvents(events, snapshot.maxChars) : "",
|
|
43
|
+
userText: eligibility.userText,
|
|
44
|
+
sourceSeqs: directUserSeqs(events),
|
|
24
45
|
eligible: eligibility.eligible,
|
|
25
46
|
...(eligibility.reason ? { skipReason: eligibility.reason } : {}),
|
|
26
47
|
};
|
|
@@ -93,6 +114,19 @@ function boundaryCursor(events) {
|
|
|
93
114
|
const seqs = events.map(eventSeq).filter((seq) => seq !== undefined);
|
|
94
115
|
return seqs.length > 0 ? `seq:${Math.max(...seqs)}` : `index:${events.length}`;
|
|
95
116
|
}
|
|
117
|
+
function sliceSnapshotEvents(snapshot, cursor) {
|
|
118
|
+
if (snapshot.cursor.startsWith("index:") && cursor.startsWith("index:") && snapshot.capturedAfterCursor?.startsWith("index:")) {
|
|
119
|
+
const start = Number(snapshot.capturedAfterCursor.slice("index:".length));
|
|
120
|
+
const phaseBoundary = Number(cursor.slice("index:".length));
|
|
121
|
+
if (!Number.isInteger(start) || !Number.isInteger(phaseBoundary))
|
|
122
|
+
return snapshot.events;
|
|
123
|
+
const relative = phaseBoundary - start;
|
|
124
|
+
if (relative <= 0)
|
|
125
|
+
return snapshot.events;
|
|
126
|
+
return relative >= snapshot.events.length ? [] : snapshot.events.slice(relative);
|
|
127
|
+
}
|
|
128
|
+
return eventsAfterCursor(snapshot.events, cursor);
|
|
129
|
+
}
|
|
96
130
|
function eventsAfterCursor(events, cursor) {
|
|
97
131
|
if (cursor === undefined)
|
|
98
132
|
return events;
|
package/lib/wrapup.js
CHANGED
|
@@ -2,7 +2,7 @@ import { ARCHIVED_AT_KEY, PROMOTED_AT_KEY, PROMOTED_TO_KEY, SOURCE_SEQS_KEY, SOU
|
|
|
2
2
|
import { MEMORY_TYPE_KEY, isMemoryType } from "./types.js";
|
|
3
3
|
import { extractJsonObject } from "./plan.js";
|
|
4
4
|
import { compactText } from "./render.js";
|
|
5
|
-
import { streamText } from "./llm-text.js";
|
|
5
|
+
import { asLlmSessionId, streamText } from "./llm-text.js";
|
|
6
6
|
import { getUsageCount, loadUsage } from "./usage.js";
|
|
7
7
|
import { createTokenUsageObserver } from "./token-usage.js";
|
|
8
8
|
import { recencyScore } from "./inject.js";
|
|
@@ -522,6 +522,7 @@ export async function assessLocalEntries(ctx, agent, candidates, options = {}) {
|
|
|
522
522
|
const text = await streamText(ctx, {
|
|
523
523
|
provider: agent.options.provider,
|
|
524
524
|
model: agent.options.model,
|
|
525
|
+
sessionId: asLlmSessionId(agent.id),
|
|
525
526
|
system: WRAPUP_ASSESS_SYSTEM_PROMPT,
|
|
526
527
|
prompt: userPrompt,
|
|
527
528
|
maxTokens: options.maxOutputTokens ?? 4096,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-continual-evolve",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
4
4
|
"description": "Continual self-evolution plugin for DeepSeek Harness: versioned, auditable, rollback-safe harness state (prompt notes, memories, skills, subagent specs) refined from session trajectories.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|