dsh-continual-evolve 0.7.1 → 0.7.3
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 +23 -14
- package/README.zh.md +18 -14
- package/lib/approval.d.ts +11 -4
- package/lib/approval.js +23 -7
- package/lib/auto.d.ts +35 -5
- package/lib/auto.js +247 -82
- package/lib/command.d.ts +2 -3
- package/lib/command.js +19 -17
- package/lib/fate.d.ts +1 -1
- package/lib/fate.js +4 -4
- package/lib/index.d.ts +4 -4
- package/lib/index.js +29 -24
- package/lib/llm-text.d.ts +48 -5
- package/lib/llm-text.js +94 -27
- package/lib/memory-agent.d.ts +111 -0
- package/lib/memory-agent.js +535 -0
- package/lib/planner.d.ts +5 -0
- package/lib/planner.js +1 -1
- package/lib/review-scheduler.d.ts +38 -0
- package/lib/review-scheduler.js +192 -0
- package/lib/review.d.ts +4 -2
- package/lib/review.js +7 -6
- package/lib/runtime.d.ts +13 -13
- package/lib/runtime.js +41 -37
- 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 +68 -0
- package/lib/turn-snapshot.js +222 -0
- package/package.json +1 -1
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serial latest-boundary scheduler for automatic memory+review snapshots.
|
|
3
|
+
*
|
|
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.
|
|
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;
|
|
21
|
+
/**
|
|
22
|
+
* Create a scheduler with ZCode-compatible serial/coalescing semantics.
|
|
23
|
+
*
|
|
24
|
+
* @param execute - one serial extraction/review operation
|
|
25
|
+
* @param cursorOf - extracts the durable boundary from a snapshot
|
|
26
|
+
*/
|
|
27
|
+
export function createReviewScheduler(execute, cursorOf) {
|
|
28
|
+
let cursor;
|
|
29
|
+
let running;
|
|
30
|
+
let shuttingDown = false;
|
|
31
|
+
let wakePending;
|
|
32
|
+
const pending = [];
|
|
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
|
+
};
|
|
99
|
+
const processSnapshot = async (snapshot) => {
|
|
100
|
+
if (shuttingDown || signal.aborted)
|
|
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
|
+
}
|
|
108
|
+
let status;
|
|
109
|
+
try {
|
|
110
|
+
status = await execute({ snapshot, signal });
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
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;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const run = async (first) => {
|
|
122
|
+
try {
|
|
123
|
+
let acquisition = await waitForSnapshotAcquisitionOrShutdown(first, signal);
|
|
124
|
+
while (acquisition.status !== "shutdown" && !shuttingDown) {
|
|
125
|
+
if (acquisition.status === "acquired")
|
|
126
|
+
await processSnapshot(acquisition.snapshot);
|
|
127
|
+
const next = await takeHighestPending();
|
|
128
|
+
if (!next)
|
|
129
|
+
break;
|
|
130
|
+
acquisition = next;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
if (shuttingDown) {
|
|
135
|
+
pending.length = 0;
|
|
136
|
+
wakePending?.();
|
|
137
|
+
wakePending = undefined;
|
|
138
|
+
}
|
|
139
|
+
running = undefined;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
return {
|
|
143
|
+
async drain() {
|
|
144
|
+
while (running)
|
|
145
|
+
await running;
|
|
146
|
+
},
|
|
147
|
+
getCursor() {
|
|
148
|
+
return cursor;
|
|
149
|
+
},
|
|
150
|
+
hasPendingWork() {
|
|
151
|
+
return running !== undefined || pending.length > 0;
|
|
152
|
+
},
|
|
153
|
+
schedule(snapshot) {
|
|
154
|
+
if (shuttingDown)
|
|
155
|
+
return;
|
|
156
|
+
const acquisition = acquireSnapshot(snapshot);
|
|
157
|
+
if (running)
|
|
158
|
+
trackPending(acquisition);
|
|
159
|
+
else
|
|
160
|
+
running = run(acquisition);
|
|
161
|
+
},
|
|
162
|
+
shutdown() {
|
|
163
|
+
if (shuttingDown)
|
|
164
|
+
return;
|
|
165
|
+
shuttingDown = true;
|
|
166
|
+
pending.length = 0;
|
|
167
|
+
shutdownController.abort();
|
|
168
|
+
wakePending?.();
|
|
169
|
+
wakePending = undefined;
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function acquireSnapshot(snapshot) {
|
|
174
|
+
return Promise.resolve(snapshot).then((value) => ({ status: "acquired", snapshot: value }), () => ({ status: "error" }));
|
|
175
|
+
}
|
|
176
|
+
function waitForSnapshotAcquisitionOrShutdown(acquisition, signal) {
|
|
177
|
+
if (signal.aborted)
|
|
178
|
+
return Promise.resolve({ status: "shutdown" });
|
|
179
|
+
return new Promise((resolve) => {
|
|
180
|
+
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
181
|
+
const onAbort = () => {
|
|
182
|
+
cleanup();
|
|
183
|
+
resolve({ status: "shutdown" });
|
|
184
|
+
};
|
|
185
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
186
|
+
void acquisition.then((result) => {
|
|
187
|
+
cleanup();
|
|
188
|
+
resolve(result);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=review-scheduler.js.map
|
package/lib/review.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export interface AutoRefineReview {
|
|
|
15
15
|
rationale: string;
|
|
16
16
|
instructions?: string;
|
|
17
17
|
}
|
|
18
|
-
export type AutoRefineReason = "turn_interval" | "compact" | "goal_blocked";
|
|
18
|
+
export type AutoRefineReason = "turn_snapshot" | "turn_interval" | "compact" | "goal_blocked";
|
|
19
19
|
export interface AutoRefineReviewContext {
|
|
20
20
|
reason: AutoRefineReason;
|
|
21
21
|
turnsSinceLastReview: number;
|
|
@@ -27,6 +27,8 @@ export interface ReviewOptions {
|
|
|
27
27
|
context: AutoRefineReviewContext;
|
|
28
28
|
/** Serialized trajectory text; when absent the gate is skipped by the caller. */
|
|
29
29
|
trajectory?: string;
|
|
30
|
+
/** Incremental surface rows captured for this turn snapshot. */
|
|
31
|
+
trajectoryEvents?: readonly unknown[];
|
|
30
32
|
signal?: AbortSignal;
|
|
31
33
|
budgetTokens?: number;
|
|
32
34
|
/** Gap C1: optional provider/model override for the review gate (cheaper model). */
|
|
@@ -42,7 +44,7 @@ export interface ReviewOptions {
|
|
|
42
44
|
/** Direct-call token ledger destination; the review phase is fixed by this module. */
|
|
43
45
|
tokenUsage?: TokenUsageTarget;
|
|
44
46
|
}
|
|
45
|
-
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}";
|
|
46
48
|
/** Parse the gate's JSON reply. */
|
|
47
49
|
export declare function parseAutoRefineReview(text: string): AutoRefineReview;
|
|
48
50
|
/** Serialize surface events to bounded role-prefixed text. */
|
package/lib/review.js
CHANGED
|
@@ -6,11 +6,12 @@ import { 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
|
|
@@ -101,7 +102,7 @@ export async function reviewAutoRefine(ctx, options) {
|
|
|
101
102
|
}
|
|
102
103
|
// Route A carries the judged conversation as a session-derived message
|
|
103
104
|
// prefix and drops the flat block; an empty prefix falls back to B.
|
|
104
|
-
const events = sessionEventsOf(agent);
|
|
105
|
+
const events = options.trajectoryEvents ?? sessionEventsOf(agent);
|
|
105
106
|
const routing = resolvePrefixCache(options.prefixCache);
|
|
106
107
|
const route = detectPlannerRoute(events, routing.mode);
|
|
107
108
|
let prefixMessages = [];
|
package/lib/runtime.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
/** On-disk runtime gate state. Version 2 adds an explicit enable bit. */
|
|
1
2
|
export interface GateRuntime {
|
|
2
|
-
version:
|
|
3
|
-
/**
|
|
3
|
+
version: 2;
|
|
4
|
+
/** Whether the human has enabled automatic review. */
|
|
5
|
+
enabled: boolean;
|
|
6
|
+
/** Whether the human has explicitly paused automatic review. */
|
|
4
7
|
paused: boolean;
|
|
5
8
|
/** ISO timestamp of the last pause/resume write. */
|
|
6
9
|
updatedAt: string;
|
|
@@ -8,17 +11,14 @@ export interface GateRuntime {
|
|
|
8
11
|
/** Full path of the runtime switch file. */
|
|
9
12
|
export declare function runtimePath(baseDir: string): string;
|
|
10
13
|
/**
|
|
11
|
-
* Load the runtime switch
|
|
12
|
-
*
|
|
13
|
-
* for a bad file).
|
|
14
|
+
* Load the runtime switch. Missing, malformed, or legacy content uses the
|
|
15
|
+
* supplied configured default and never throws; this is a trigger-path read.
|
|
14
16
|
*/
|
|
15
|
-
export declare function loadGateRuntime(baseDir: string): GateRuntime;
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
* through the command's own error path instead.
|
|
20
|
-
*/
|
|
21
|
-
export declare function saveGateRuntime(baseDir: string, paused: boolean): string;
|
|
22
|
-
/** True while the human paused the automatic review gate (fail-open). */
|
|
17
|
+
export declare function loadGateRuntime(baseDir: string, defaultEnabled?: boolean): GateRuntime;
|
|
18
|
+
/** Persist the runtime switch atomically (temp file + rename). */
|
|
19
|
+
export declare function saveGateRuntime(baseDir: string, paused: boolean, enabled?: boolean): string;
|
|
20
|
+
/** True while the human paused the automatic review gate. */
|
|
23
21
|
export declare function isGatePaused(baseDir: string): boolean;
|
|
22
|
+
/** Effective automatic-review state, including the explicit enable bit. */
|
|
23
|
+
export declare function isGateEnabled(baseDir: string, defaultEnabled?: boolean): boolean;
|
|
24
24
|
//# sourceMappingURL=runtime.d.ts.map
|
package/lib/runtime.js
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Runtime gate switch
|
|
3
|
-
* automatic review
|
|
2
|
+
* Runtime gate switch: a small human-owned file that enables or pauses the
|
|
3
|
+
* automatic review listener without editing the profile or restarting DSH.
|
|
4
4
|
*
|
|
5
|
-
* - file: `<baseDir>/evolve/runtime.json` (`{version, paused, updatedAt}`)
|
|
6
|
-
* -
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* exceptional state; a broken file must never silently wedge the gate
|
|
12
|
-
* shut (the next save rewrites it cleanly).
|
|
5
|
+
* - file: `<baseDir>/evolve/runtime.json` (`{version: 2, enabled, paused, updatedAt}`)
|
|
6
|
+
* - the file is consulted at every trigger and before scheduler execution;
|
|
7
|
+
* - `/evolve pause` sets `paused`, `/evolve resume` sets `enabled=true` and
|
|
8
|
+
* clears the pause, while manual tools and commands remain independent;
|
|
9
|
+
* - a missing or malformed file uses the caller's configured default. A
|
|
10
|
+
* legacy v1 `{paused}` file is readable and preserves its pause state.
|
|
13
11
|
*/
|
|
14
|
-
import { existsSync, mkdirSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
15
|
-
import { readFileSync } from "node:fs";
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
16
13
|
import { randomUUID } from "node:crypto";
|
|
17
14
|
import { join } from "node:path";
|
|
18
15
|
import { EVOLVE_DIR } from "./store.js";
|
|
@@ -21,57 +18,54 @@ const RUNTIME_FILE = "runtime.json";
|
|
|
21
18
|
export function runtimePath(baseDir) {
|
|
22
19
|
return join(baseDir, EVOLVE_DIR, RUNTIME_FILE);
|
|
23
20
|
}
|
|
24
|
-
function
|
|
25
|
-
return { version:
|
|
21
|
+
function state(enabled, paused) {
|
|
22
|
+
return { version: 2, enabled, paused, updatedAt: new Date(0).toISOString() };
|
|
26
23
|
}
|
|
27
24
|
/**
|
|
28
|
-
* Load the runtime switch
|
|
29
|
-
*
|
|
30
|
-
* for a bad file).
|
|
25
|
+
* Load the runtime switch. Missing, malformed, or legacy content uses the
|
|
26
|
+
* supplied configured default and never throws; this is a trigger-path read.
|
|
31
27
|
*/
|
|
32
|
-
export function loadGateRuntime(baseDir) {
|
|
28
|
+
export function loadGateRuntime(baseDir, defaultEnabled = false) {
|
|
33
29
|
const path = runtimePath(baseDir);
|
|
34
|
-
if (!existsSync(path))
|
|
35
|
-
return
|
|
36
|
-
}
|
|
30
|
+
if (!existsSync(path))
|
|
31
|
+
return state(defaultEnabled, false);
|
|
37
32
|
try {
|
|
38
33
|
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
39
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
40
|
-
return
|
|
41
|
-
}
|
|
34
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
35
|
+
return state(defaultEnabled, false);
|
|
42
36
|
const record = raw;
|
|
37
|
+
const paused = record["paused"] === true;
|
|
38
|
+
// v1 had no enable bit. Its only meaningful state was pause, so retain
|
|
39
|
+
// the configured default when it is not paused.
|
|
40
|
+
const enabled = typeof record["enabled"] === "boolean" ? record["enabled"] : defaultEnabled;
|
|
43
41
|
return {
|
|
44
|
-
version:
|
|
45
|
-
|
|
42
|
+
version: 2,
|
|
43
|
+
enabled,
|
|
44
|
+
paused,
|
|
46
45
|
updatedAt: typeof record["updatedAt"] === "string" ? record["updatedAt"] : new Date(0).toISOString(),
|
|
47
46
|
};
|
|
48
47
|
}
|
|
49
48
|
catch {
|
|
50
|
-
return
|
|
49
|
+
return state(defaultEnabled, false);
|
|
51
50
|
}
|
|
52
51
|
}
|
|
53
|
-
/**
|
|
54
|
-
|
|
55
|
-
* for a bad file — callers (pause/resume commands) surface real IO errors
|
|
56
|
-
* through the command's own error path instead.
|
|
57
|
-
*/
|
|
58
|
-
export function saveGateRuntime(baseDir, paused) {
|
|
52
|
+
/** Persist the runtime switch atomically (temp file + rename). */
|
|
53
|
+
export function saveGateRuntime(baseDir, paused, enabled = true) {
|
|
59
54
|
const path = runtimePath(baseDir);
|
|
60
55
|
mkdirSync(join(baseDir, EVOLVE_DIR), { recursive: true });
|
|
61
56
|
const tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
62
|
-
const payload = { version:
|
|
57
|
+
const payload = { version: 2, enabled, paused, updatedAt: new Date().toISOString() };
|
|
63
58
|
writeFileSync(tempPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
64
59
|
try {
|
|
65
60
|
renameSync(tempPath, path);
|
|
66
61
|
}
|
|
67
62
|
finally {
|
|
68
|
-
if (existsSync(tempPath))
|
|
63
|
+
if (existsSync(tempPath))
|
|
69
64
|
unlinkSync(tempPath);
|
|
70
|
-
}
|
|
71
65
|
}
|
|
72
66
|
return path;
|
|
73
67
|
}
|
|
74
|
-
/** True while the human paused the automatic review gate
|
|
68
|
+
/** True while the human paused the automatic review gate. */
|
|
75
69
|
export function isGatePaused(baseDir) {
|
|
76
70
|
try {
|
|
77
71
|
return loadGateRuntime(baseDir).paused;
|
|
@@ -80,4 +74,14 @@ export function isGatePaused(baseDir) {
|
|
|
80
74
|
return false;
|
|
81
75
|
}
|
|
82
76
|
}
|
|
77
|
+
/** Effective automatic-review state, including the explicit enable bit. */
|
|
78
|
+
export function isGateEnabled(baseDir, defaultEnabled = false) {
|
|
79
|
+
try {
|
|
80
|
+
const current = loadGateRuntime(baseDir, defaultEnabled);
|
|
81
|
+
return current.enabled && !current.paused;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return defaultEnabled;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
83
87
|
//# sourceMappingURL=runtime.js.map
|
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) {
|