dsh-continual-evolve 0.11.0 → 0.12.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 +42 -115
- package/README.zh.md +33 -93
- package/client/client.js +253 -0
- package/cordis.patch.yml +2 -2
- package/lib/card-routes.d.ts +32 -0
- package/lib/card-routes.js +92 -0
- package/lib/index.d.ts +65 -217
- package/lib/index.js +74 -234
- package/lib/known-workspaces.d.ts +22 -0
- package/lib/known-workspaces.js +42 -0
- package/lib/memory-guide.d.ts +13 -14
- package/lib/memory-guide.js +34 -21
- package/lib/memory-section.d.ts +77 -0
- package/lib/memory-section.js +189 -0
- package/lib/memory-snapshot.d.ts +54 -0
- package/lib/memory-snapshot.js +125 -0
- package/package.json +8 -17
- package/lib/apply.d.ts +0 -22
- package/lib/apply.js +0 -157
- package/lib/approval.d.ts +0 -73
- package/lib/approval.js +0 -127
- package/lib/audit-log.d.ts +0 -19
- package/lib/audit-log.js +0 -52
- package/lib/auto.d.ts +0 -236
- package/lib/auto.js +0 -773
- package/lib/autocase.d.ts +0 -38
- package/lib/autocase.js +0 -76
- package/lib/benchmark-command.d.ts +0 -9
- package/lib/benchmark-command.js +0 -357
- package/lib/benchmark.d.ts +0 -151
- package/lib/benchmark.js +0 -250
- package/lib/command-util.d.ts +0 -70
- package/lib/command-util.js +0 -123
- package/lib/command.d.ts +0 -13
- package/lib/command.js +0 -134
- package/lib/consolidate.d.ts +0 -90
- package/lib/consolidate.js +0 -180
- package/lib/copy.d.ts +0 -37
- package/lib/copy.js +0 -58
- package/lib/declines.d.ts +0 -103
- package/lib/declines.js +0 -179
- package/lib/evaluate.d.ts +0 -74
- package/lib/evaluate.js +0 -271
- package/lib/evolve-event.d.ts +0 -40
- package/lib/evolve-event.js +0 -60
- package/lib/extraction-phase.d.ts +0 -132
- package/lib/extraction-phase.js +0 -236
- package/lib/failures.d.ts +0 -42
- package/lib/failures.js +0 -171
- package/lib/fate.d.ts +0 -130
- package/lib/fate.js +0 -350
- package/lib/goal-command.d.ts +0 -7
- package/lib/goal-command.js +0 -37
- package/lib/goal.d.ts +0 -72
- package/lib/goal.js +0 -72
- package/lib/inject.d.ts +0 -90
- package/lib/inject.js +0 -164
- package/lib/injection-caps.d.ts +0 -4
- package/lib/injection-caps.js +0 -4
- package/lib/injection-directory.d.ts +0 -29
- package/lib/injection-directory.js +0 -78
- package/lib/injection-rank.d.ts +0 -85
- package/lib/injection-rank.js +0 -161
- package/lib/listener.d.ts +0 -45
- package/lib/listener.js +0 -199
- package/lib/llm-text.d.ts +0 -104
- package/lib/llm-text.js +0 -140
- package/lib/logfile.d.ts +0 -71
- package/lib/logfile.js +0 -159
- package/lib/maintenance-commands.d.ts +0 -23
- package/lib/maintenance-commands.js +0 -136
- package/lib/memory-agent.d.ts +0 -93
- package/lib/memory-agent.js +0 -300
- package/lib/memory-apply.d.ts +0 -44
- package/lib/memory-apply.js +0 -202
- package/lib/memory-benchmark.d.ts +0 -76
- package/lib/memory-benchmark.js +0 -170
- package/lib/memory-index.d.ts +0 -114
- package/lib/memory-index.js +0 -197
- package/lib/memory-manifest.d.ts +0 -16
- package/lib/memory-manifest.js +0 -70
- package/lib/message-source.d.ts +0 -23
- package/lib/message-source.js +0 -12
- package/lib/mount-command.d.ts +0 -10
- package/lib/mount-command.js +0 -48
- package/lib/mount.d.ts +0 -42
- package/lib/mount.js +0 -212
- package/lib/notify.d.ts +0 -48
- package/lib/notify.js +0 -57
- package/lib/plan-command.d.ts +0 -12
- package/lib/plan-command.js +0 -38
- package/lib/plan.d.ts +0 -16
- package/lib/plan.js +0 -126
- package/lib/planner.d.ts +0 -61
- package/lib/planner.js +0 -144
- package/lib/pool.d.ts +0 -7
- package/lib/pool.js +0 -25
- package/lib/prefix-cache.d.ts +0 -79
- package/lib/prefix-cache.js +0 -168
- package/lib/project.d.ts +0 -28
- package/lib/project.js +0 -66
- package/lib/projection.d.ts +0 -22
- package/lib/projection.js +0 -112
- package/lib/promotion.d.ts +0 -94
- package/lib/promotion.js +0 -171
- package/lib/recall.d.ts +0 -85
- package/lib/recall.js +0 -142
- package/lib/record-language.d.ts +0 -85
- package/lib/record-language.js +0 -152
- package/lib/render.d.ts +0 -13
- package/lib/render.js +0 -80
- package/lib/review-scheduler.d.ts +0 -38
- package/lib/review-scheduler.js +0 -183
- package/lib/review.d.ts +0 -60
- package/lib/review.js +0 -145
- package/lib/rollback.d.ts +0 -9
- package/lib/rollback.js +0 -68
- package/lib/rubric.d.ts +0 -29
- package/lib/rubric.js +0 -119
- package/lib/runtime-commands.d.ts +0 -14
- package/lib/runtime-commands.js +0 -115
- package/lib/runtime.d.ts +0 -24
- package/lib/runtime.js +0 -87
- package/lib/score.d.ts +0 -64
- package/lib/score.js +0 -191
- package/lib/search.d.ts +0 -83
- package/lib/search.js +0 -136
- package/lib/service.d.ts +0 -63
- package/lib/service.js +0 -253
- package/lib/skill-render.d.ts +0 -23
- package/lib/skill-render.js +0 -69
- package/lib/skill.d.ts +0 -15
- package/lib/skill.js +0 -80
- package/lib/skillquality.d.ts +0 -85
- package/lib/skillquality.js +0 -320
- package/lib/source.d.ts +0 -30
- package/lib/source.js +0 -43
- package/lib/state.d.ts +0 -26
- package/lib/state.js +0 -151
- package/lib/store.d.ts +0 -61
- package/lib/store.js +0 -164
- package/lib/token-usage.d.ts +0 -68
- package/lib/token-usage.js +0 -211
- package/lib/tool.d.ts +0 -34
- package/lib/tool.js +0 -320
- package/lib/turn-snapshot.d.ts +0 -78
- package/lib/turn-snapshot.js +0 -252
- package/lib/types.d.ts +0 -218
- package/lib/types.js +0 -133
- package/lib/usage.d.ts +0 -36
- package/lib/usage.js +0 -98
- package/lib/validate.d.ts +0 -28
- package/lib/validate.js +0 -181
- package/lib/wrapup-candidates.d.ts +0 -127
- package/lib/wrapup-candidates.js +0 -161
- package/lib/wrapup-command.d.ts +0 -10
- package/lib/wrapup-command.js +0 -243
- package/lib/wrapup-proposals.d.ts +0 -101
- package/lib/wrapup-proposals.js +0 -281
- package/lib/wrapup.d.ts +0 -56
- package/lib/wrapup.js +0 -105
package/lib/auto.js
DELETED
|
@@ -1,773 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The automatic evolution driver: watches agent turns and session compaction.
|
|
3
|
-
* In the production wiring it runs only the dedicated Memory Agent; the
|
|
4
|
-
* general review/planner and local-fate phases remain available to direct
|
|
5
|
-
* callers but are not entered by memory-only mode. All auxiliary work is
|
|
6
|
-
* fire-and-forget with error containment: an automatic failure never disturbs
|
|
7
|
-
* the agent loop.
|
|
8
|
-
*
|
|
9
|
-
* Every gate decision (approved / declined / failed / skipped) is appended to
|
|
10
|
-
* `<dshHome>/evolve/reviews.jsonl` so auto-review activity is durably
|
|
11
|
-
* auditable — the server console is not a reliable place to look.
|
|
12
|
-
*
|
|
13
|
-
* Hook wiring:
|
|
14
|
-
* - `agent/turn-stopping` records the successful turn boundary.
|
|
15
|
-
* - `agent/status` (idle) captures an incremental snapshot and feeds the
|
|
16
|
-
* per-session latest-pending serial scheduler.
|
|
17
|
-
* - `session/event` (compaction/start) forces a snapshot flush before data is
|
|
18
|
-
* summarized away.
|
|
19
|
-
*/
|
|
20
|
-
import { appendFileSync, mkdirSync } from "node:fs";
|
|
21
|
-
import { join } from "node:path";
|
|
22
|
-
import { slug } from "./types.js";
|
|
23
|
-
import { planWithLlm } from "./planner.js";
|
|
24
|
-
import { reviewAutoRefine } from "./review.js";
|
|
25
|
-
import { goalServiceOf } from "./goal.js";
|
|
26
|
-
import { notifyAutoReview, notifyMemoryExtraction } from "./notify.js";
|
|
27
|
-
import { runLocalFatePhase } from "./fate.js";
|
|
28
|
-
import { mergeHarnessStates } from "./state.js";
|
|
29
|
-
import { projectKeyOf } from "./project.js";
|
|
30
|
-
import { questionServiceOf } from "./approval.js";
|
|
31
|
-
import { buildEvolveCompleteEvent, emitEvolveComplete } from "./evolve-event.js";
|
|
32
|
-
import { DEFAULT_REVIEWS_RETAIN, pruneJsonlFile } from "./store.js";
|
|
33
|
-
import { isGateEnabled } from "./runtime.js";
|
|
34
|
-
import { compareReviewCursors, createReviewScheduler } from "./review-scheduler.js";
|
|
35
|
-
import { captureTurnSnapshot, sliceTurnSnapshot } from "./turn-snapshot.js";
|
|
36
|
-
import { captureAutoCase } from "./autocase.js";
|
|
37
|
-
import { loadDeclinedMemory, matchDeclinedCheckpoint } from "./declines.js";
|
|
38
|
-
import { tokenize } from "./search.js";
|
|
39
|
-
import { applyMemoryExtractionProposal, buildMemoryManifest, runMemoryAgent, } from "./memory-agent.js";
|
|
40
|
-
import { asLlmSessionId } from "./llm-text.js";
|
|
41
|
-
import { resolveRecordLanguage } from "./record-language.js";
|
|
42
|
-
import { resolveDialogLanguage, skillConsultCopy, skillEditLine } from "./copy.js";
|
|
43
|
-
/** Turns a rejected skill candidate stays silent before being offered again. */
|
|
44
|
-
export const SKILL_CONSULT_COOLDOWN_TURNS = 10;
|
|
45
|
-
/** Default session-close drain budget: an in-flight extraction gets 15s to settle. */
|
|
46
|
-
export const SESSION_CLOSE_DRAIN_MS_DEFAULT = 15000;
|
|
47
|
-
/**
|
|
48
|
-
* Resolve the session-close drain budget: absent/invalid → default, 0 →
|
|
49
|
-
* immediate abort (legacy). Negative or non-finite values fail back to the
|
|
50
|
-
* default rather than silently disabling the drain.
|
|
51
|
-
*/
|
|
52
|
-
export function resolveSessionCloseDrainMs(raw) {
|
|
53
|
-
if (raw === undefined)
|
|
54
|
-
return SESSION_CLOSE_DRAIN_MS_DEFAULT;
|
|
55
|
-
if (typeof raw !== "number" || !Number.isFinite(raw) || raw < 0)
|
|
56
|
-
return SESSION_CLOSE_DRAIN_MS_DEFAULT;
|
|
57
|
-
return Math.floor(raw);
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Bounded session-close drain for one scheduler: wait for in-flight work up
|
|
61
|
-
* to drainMs, then abort the owner signal and drop pending captures. Never
|
|
62
|
-
* rejects — disposal must not hang the host on a stuck model call, and the
|
|
63
|
-
* abort below is idempotent when the drain already settled.
|
|
64
|
-
*/
|
|
65
|
-
export function drainSchedulerOnDispose(scheduler, drainMs) {
|
|
66
|
-
if (drainMs <= 0) {
|
|
67
|
-
try {
|
|
68
|
-
scheduler.shutdown();
|
|
69
|
-
}
|
|
70
|
-
catch {
|
|
71
|
-
// shutdown is flag-setting plus abort: nothing observable to do
|
|
72
|
-
}
|
|
73
|
-
return Promise.resolve();
|
|
74
|
-
}
|
|
75
|
-
let timer;
|
|
76
|
-
const timeout = new Promise((resolve) => {
|
|
77
|
-
timer = setTimeout(resolve, drainMs);
|
|
78
|
-
});
|
|
79
|
-
const settle = () => {
|
|
80
|
-
if (timer !== undefined)
|
|
81
|
-
clearTimeout(timer);
|
|
82
|
-
try {
|
|
83
|
-
scheduler.shutdown();
|
|
84
|
-
}
|
|
85
|
-
catch {
|
|
86
|
-
// same as above — the abort is best-effort by design
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
return Promise.race([scheduler.drain(), timeout]).then(settle, settle);
|
|
90
|
-
}
|
|
91
|
-
export function registerAutoReview(ctx, engine, config) {
|
|
92
|
-
const perSession = new Map();
|
|
93
|
-
const schedulers = new Map();
|
|
94
|
-
const captureTails = new Map();
|
|
95
|
-
const disposedSessions = new Set();
|
|
96
|
-
const logger = ctx.logger("continual-evolve");
|
|
97
|
-
const reviewsPath = join(engine.baseDir, "evolve", "reviews.jsonl");
|
|
98
|
-
const defaultEnabled = config.enabledByDefault ?? true;
|
|
99
|
-
const record = (entry) => {
|
|
100
|
-
try {
|
|
101
|
-
mkdirSync(join(engine.baseDir, "evolve"), { recursive: true });
|
|
102
|
-
appendFileSync(reviewsPath, `${JSON.stringify({ ...entry, timestamp: new Date().toISOString() })}\n`, "utf8");
|
|
103
|
-
}
|
|
104
|
-
catch (cause) {
|
|
105
|
-
logger.warn(`failed to record auto-review: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
// Storage hygiene (#20): bound the audit trail at write time.
|
|
109
|
-
try {
|
|
110
|
-
pruneJsonlFile(reviewsPath, config.reviewsRetain ?? DEFAULT_REVIEWS_RETAIN);
|
|
111
|
-
}
|
|
112
|
-
catch {
|
|
113
|
-
// ignored — the next record retries
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
|
-
const schedulerFor = (agent, state) => {
|
|
117
|
-
const existing = schedulers.get(agent.id);
|
|
118
|
-
if (existing)
|
|
119
|
-
return existing;
|
|
120
|
-
const scheduler = createReviewScheduler(async ({ snapshot, signal }) => {
|
|
121
|
-
if (!isGateEnabled(engine.baseDir, defaultEnabled))
|
|
122
|
-
return "aborted";
|
|
123
|
-
if (!snapshot.eligible) {
|
|
124
|
-
record({
|
|
125
|
-
sessionId: snapshot.sessionId,
|
|
126
|
-
reason: snapshot.reason,
|
|
127
|
-
turnsSinceLastReview: state.turns - state.lastReviewAt,
|
|
128
|
-
outcome: "skipped",
|
|
129
|
-
rationale: `snapshot ${snapshot.cursor} skipped: ${snapshot.skipReason ?? "not eligible"}`,
|
|
130
|
-
});
|
|
131
|
-
return "no-op";
|
|
132
|
-
}
|
|
133
|
-
try {
|
|
134
|
-
if (config.memoryOnly) {
|
|
135
|
-
await runMemoryExtractionPhase(ctx, engine, agent, config, state, snapshot, record, signal);
|
|
136
|
-
}
|
|
137
|
-
else {
|
|
138
|
-
await runGate(ctx, engine, agent, config, state, snapshot, record, signal);
|
|
139
|
-
}
|
|
140
|
-
return "success";
|
|
141
|
-
}
|
|
142
|
-
catch (cause) {
|
|
143
|
-
const message = cause instanceof Error ? cause.message : String(cause);
|
|
144
|
-
logger.warn(`auto-review failed for ${agent.id}: ${message}`);
|
|
145
|
-
record({
|
|
146
|
-
sessionId: agent.id,
|
|
147
|
-
reason: snapshot.reason,
|
|
148
|
-
turnsSinceLastReview: state.turns - state.lastReviewAt,
|
|
149
|
-
outcome: "failed",
|
|
150
|
-
rationale: `gate error: ${message}`,
|
|
151
|
-
});
|
|
152
|
-
return "error";
|
|
153
|
-
}
|
|
154
|
-
}, (snapshot) => snapshot.cursor);
|
|
155
|
-
schedulers.set(agent.id, scheduler);
|
|
156
|
-
return scheduler;
|
|
157
|
-
};
|
|
158
|
-
const captureAndSchedule = (agent, state, reason) => {
|
|
159
|
-
if (disposedSessions.has(agent.id) || !isGateEnabled(engine.baseDir, defaultEnabled))
|
|
160
|
-
return;
|
|
161
|
-
const turn = state.completedTurn;
|
|
162
|
-
const previousSnapshotTurn = state.lastSnapshotTurn;
|
|
163
|
-
if (reason === "turn_snapshot" && turn <= previousSnapshotTurn)
|
|
164
|
-
return;
|
|
165
|
-
if (reason === "turn_snapshot")
|
|
166
|
-
state.lastSnapshotTurn = turn;
|
|
167
|
-
const previousCapture = captureTails.get(agent.id) ?? Promise.resolve();
|
|
168
|
-
const captureTask = previousCapture.then(async () => {
|
|
169
|
-
if (disposedSessions.has(agent.id) || !isGateEnabled(engine.baseDir, defaultEnabled))
|
|
170
|
-
return;
|
|
171
|
-
const cursor = schedulerFor(agent, state).getCursor();
|
|
172
|
-
const snapshot = await captureTurnSnapshot(ctx, agent, {
|
|
173
|
-
turn,
|
|
174
|
-
reason,
|
|
175
|
-
...(cursor !== undefined ? { cursor } : {}),
|
|
176
|
-
maxChars: config.maxInputChars,
|
|
177
|
-
...(config.memoryMinUserWords !== undefined ? { minUserWords: config.memoryMinUserWords } : {}),
|
|
178
|
-
});
|
|
179
|
-
if (!disposedSessions.has(agent.id))
|
|
180
|
-
schedulerFor(agent, state).schedule(snapshot);
|
|
181
|
-
}).catch((cause) => {
|
|
182
|
-
const message = cause instanceof Error ? cause.message : String(cause);
|
|
183
|
-
logger.warn(`auto-review snapshot failed for ${agent.id}: ${message}`);
|
|
184
|
-
record({
|
|
185
|
-
sessionId: agent.id,
|
|
186
|
-
reason,
|
|
187
|
-
turnsSinceLastReview: state.turns - state.lastReviewAt,
|
|
188
|
-
outcome: "failed",
|
|
189
|
-
rationale: `snapshot error: ${message}`,
|
|
190
|
-
});
|
|
191
|
-
if (reason === "turn_snapshot" && state.lastSnapshotTurn === turn) {
|
|
192
|
-
state.lastSnapshotTurn = previousSnapshotTurn;
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
captureTails.set(agent.id, captureTask);
|
|
196
|
-
};
|
|
197
|
-
// DSH emits this awaited boundary when a turn is about to close. The idle
|
|
198
|
-
// transition is the safe point at which to read the durable surface.
|
|
199
|
-
ctx.on("agent/turn-stopping", (payload) => {
|
|
200
|
-
const agent = payload.agent;
|
|
201
|
-
if (!agent) {
|
|
202
|
-
logger.warn(`auto-review gate: agent/turn-stopping payload missing agent; skipping count`);
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
const state = stateFor(perSession, agent.id);
|
|
206
|
-
state.turns += 1;
|
|
207
|
-
if (typeof payload.turn === "number")
|
|
208
|
-
state.completedTurn = Math.max(state.completedTurn, payload.turn);
|
|
209
|
-
});
|
|
210
|
-
ctx.on("agent/status", (payload) => {
|
|
211
|
-
const agent = payload.agent;
|
|
212
|
-
if (!agent || payload.status !== "idle")
|
|
213
|
-
return;
|
|
214
|
-
const state = stateFor(perSession, agent.id);
|
|
215
|
-
if (state.completedTurn <= state.lastSnapshotTurn)
|
|
216
|
-
return;
|
|
217
|
-
captureAndSchedule(agent, state, "turn_snapshot");
|
|
218
|
-
});
|
|
219
|
-
// Diagnostic: the armed marker proves the listener was registered even when
|
|
220
|
-
// its runtime default is off, distinguishing "not registered" from "off".
|
|
221
|
-
try {
|
|
222
|
-
mkdirSync(join(engine.baseDir, "evolve"), { recursive: true });
|
|
223
|
-
appendFileSync(reviewsPath, `${JSON.stringify({
|
|
224
|
-
timestamp: new Date().toISOString(),
|
|
225
|
-
sessionId: "boot",
|
|
226
|
-
reason: "boot",
|
|
227
|
-
turnsSinceLastReview: 0,
|
|
228
|
-
outcome: "armed",
|
|
229
|
-
rationale: `automatic evolution listener registered (mode=${config.memoryOnly ? "memory-only" : "full"}; default=${defaultEnabled ? "on" : "off"}; per-success-turn snapshots)`,
|
|
230
|
-
})}\n`, "utf8");
|
|
231
|
-
}
|
|
232
|
-
catch (cause) {
|
|
233
|
-
logger.warn(`failed to write armed marker: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
234
|
-
}
|
|
235
|
-
try {
|
|
236
|
-
pruneJsonlFile(reviewsPath, config.reviewsRetain ?? DEFAULT_REVIEWS_RETAIN);
|
|
237
|
-
}
|
|
238
|
-
catch {
|
|
239
|
-
// ignored — the next record retries
|
|
240
|
-
}
|
|
241
|
-
ctx.on("agent/disposed", (payload) => {
|
|
242
|
-
const agent = payload.agent;
|
|
243
|
-
if (!agent)
|
|
244
|
-
return;
|
|
245
|
-
disposedSessions.add(agent.id);
|
|
246
|
-
const scheduler = schedulers.get(agent.id);
|
|
247
|
-
schedulers.delete(agent.id);
|
|
248
|
-
captureTails.delete(agent.id);
|
|
249
|
-
perSession.delete(agent.id);
|
|
250
|
-
if (scheduler) {
|
|
251
|
-
// Bounded drain (P1): an in-flight extraction gets drainMs to
|
|
252
|
-
// settle so its result is not lost on session close; afterwards
|
|
253
|
-
// the owner signal aborts. Fire-and-forget — disposal must never
|
|
254
|
-
// hang the host, and the helper never rejects.
|
|
255
|
-
void drainSchedulerOnDispose(scheduler, resolveSessionCloseDrainMs(config.sessionCloseDrainMs));
|
|
256
|
-
}
|
|
257
|
-
});
|
|
258
|
-
ctx.on("session/event", (session, event) => {
|
|
259
|
-
// ZCode schedules extraction from successful turns, not from compaction.
|
|
260
|
-
// Keep the legacy compaction path for direct/full callers, but do not
|
|
261
|
-
// add an extra automatic Memory Agent trigger in memory-only mode.
|
|
262
|
-
if (config.memoryOnly)
|
|
263
|
-
return;
|
|
264
|
-
if (event.type !== "compaction/start")
|
|
265
|
-
return;
|
|
266
|
-
const agents = ctx.agents;
|
|
267
|
-
const agent = agents?.get(session.id);
|
|
268
|
-
if (!agent)
|
|
269
|
-
return; // no live agent for that session (e.g. cold read)
|
|
270
|
-
const state = stateFor(perSession, agent.id);
|
|
271
|
-
captureAndSchedule(agent, state, "compact");
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
/**
|
|
275
|
-
* Gap C1: parse a "provider/model" or "model" string into its components.
|
|
276
|
-
* Returns undefined when the input is empty (no override). A bare model name
|
|
277
|
-
* falls back to the agent's provider, then "deepseek".
|
|
278
|
-
* Exported for unit testing (the advanceGateState precedent); production
|
|
279
|
-
* resolves it inside runReviewPhase.
|
|
280
|
-
*/
|
|
281
|
-
export function parseReviewModel(reviewModel, fallbackProvider) {
|
|
282
|
-
if (!reviewModel || reviewModel.trim().length === 0)
|
|
283
|
-
return undefined;
|
|
284
|
-
const slash = reviewModel.indexOf("/");
|
|
285
|
-
if (slash > 0) {
|
|
286
|
-
return { provider: reviewModel.slice(0, slash), model: reviewModel.slice(slash + 1) };
|
|
287
|
-
}
|
|
288
|
-
return { provider: fallbackProvider ?? "deepseek", model: reviewModel };
|
|
289
|
-
}
|
|
290
|
-
function stateFor(map, sessionId) {
|
|
291
|
-
let state = map.get(sessionId);
|
|
292
|
-
if (!state) {
|
|
293
|
-
state = {
|
|
294
|
-
turns: 0,
|
|
295
|
-
completedTurn: 0,
|
|
296
|
-
lastSnapshotTurn: 0,
|
|
297
|
-
memoryDecisions: {},
|
|
298
|
-
lastReviewAt: 0,
|
|
299
|
-
running: false,
|
|
300
|
-
skillRejects: new Map(),
|
|
301
|
-
lastFateAt: 0,
|
|
302
|
-
fateRejects: new Map(),
|
|
303
|
-
goalBlockStreak: 0,
|
|
304
|
-
};
|
|
305
|
-
map.set(sessionId, state);
|
|
306
|
-
}
|
|
307
|
-
return state;
|
|
308
|
-
}
|
|
309
|
-
function advanceMemoryCheckpoint(state, next) {
|
|
310
|
-
if (state.memoryCheckpoint === undefined) {
|
|
311
|
-
state.memoryCheckpoint = next;
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
const order = compareReviewCursors(next, state.memoryCheckpoint);
|
|
315
|
-
if (order === undefined || order > 0)
|
|
316
|
-
state.memoryCheckpoint = next;
|
|
317
|
-
}
|
|
318
|
-
function rememberMemoryDecision(state, key, decision) {
|
|
319
|
-
state.memoryDecisions[key] = decision;
|
|
320
|
-
const keys = Object.keys(state.memoryDecisions);
|
|
321
|
-
while (keys.length > 128)
|
|
322
|
-
delete state.memoryDecisions[keys.shift() ?? ""];
|
|
323
|
-
}
|
|
324
|
-
/**
|
|
325
|
-
* The state view the gate and planner judge: the session's local entries
|
|
326
|
-
* merged with the project and global stores, each entry carrying its real
|
|
327
|
-
* scope. Without the global half the gate cannot see that a topic is already
|
|
328
|
-
* covered cross-session and happily re-sediments a local duplicate of it.
|
|
329
|
-
*
|
|
330
|
-
* The merged view is read-only context — applying still targets the raw
|
|
331
|
-
* local state (baseline checks compare local entries only).
|
|
332
|
-
*/
|
|
333
|
-
export function loadGateHarnessView(engine, sessionId, opts) {
|
|
334
|
-
const projectState = opts?.projectKey ? engine.load("project", opts.projectKey) : undefined;
|
|
335
|
-
return mergeHarnessStates(engine.load("global", undefined), engine.load("local", sessionId), projectState ? { projectState } : undefined);
|
|
336
|
-
}
|
|
337
|
-
/**
|
|
338
|
-
* One gate run = review phase + local-fate phase (#11 P2). The review phase
|
|
339
|
-
* judges and applies local refinements; the local-fate phase then gives the
|
|
340
|
-
* session's existing local entries a running exit (promote/archive proposals,
|
|
341
|
-
* consulted before they land). Running fate AFTER the review keeps the
|
|
342
|
-
* review's baseline fresh — fate re-loads the store and never races the
|
|
343
|
-
* review's optimistic-concurrency checks.
|
|
344
|
-
*/
|
|
345
|
-
async function runGate(ctx, engine, agent, config, state, snapshot, record, signal) {
|
|
346
|
-
// Reentry guard (review audit 2026-08-28 S3): a gate run holds LLM calls
|
|
347
|
-
// and possibly a user question for a long time; an idle/compaction
|
|
348
|
-
// trigger overlapping the run would start a second concurrent pipeline
|
|
349
|
-
// whose stale whole-file saves clobber the first run's writes.
|
|
350
|
-
if (state.running) {
|
|
351
|
-
throw new Error(`previous gate run still in flight for ${agent.id}`);
|
|
352
|
-
}
|
|
353
|
-
state.running = true;
|
|
354
|
-
try {
|
|
355
|
-
await runMemoryExtractionPhase(ctx, engine, agent, config, state, snapshot, record, signal);
|
|
356
|
-
await runReviewPhase(ctx, engine, agent, config, state, snapshot, record, signal);
|
|
357
|
-
// D3: a goal stuck in "blocked" for consecutive gate runs gets one
|
|
358
|
-
// local-fate assessment (the pipeline below), so whatever led the
|
|
359
|
-
// goal astray is distilled before the session moves on.
|
|
360
|
-
await runGoalBlockedFate(ctx, engine, agent, config, state, snapshot.reason, record);
|
|
361
|
-
await runLocalFatePhase(ctx, engine, agent, config, state, snapshot.reason, record);
|
|
362
|
-
}
|
|
363
|
-
finally {
|
|
364
|
-
state.running = false;
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
/**
|
|
368
|
-
* D3 (goal blocked → wrap-up coupling, reverse direction): count consecutive
|
|
369
|
-
* gate runs whose goal is in phase "blocked"; when the streak reaches
|
|
370
|
-
* `goalBlockedWrapupTurns`, run ONE local-fate assessment (same pipeline as
|
|
371
|
-
* the normal fate dimension — audit, classify, consult, apply deterministically).
|
|
372
|
-
* The streak resets on any non-blocked run and after a triggered assessment;
|
|
373
|
-
* a declined proposal is then protected by the normal fate cooldown, so a
|
|
374
|
-
* blocked session can never be nagged into another dialog.
|
|
375
|
-
*
|
|
376
|
-
* Exported for unit testing (the advanceGateState precedent); production runs
|
|
377
|
-
* it from runGate.
|
|
378
|
-
*/
|
|
379
|
-
export async function runGoalBlockedFate(ctx, engine, agent, config, state, _reason, record) {
|
|
380
|
-
if (config.goalBlockedWrapupTurns <= 0)
|
|
381
|
-
return;
|
|
382
|
-
const goal = goalServiceOf(ctx)?.get(agent);
|
|
383
|
-
if (goal?.phase !== "blocked") {
|
|
384
|
-
state.goalBlockStreak = 0;
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
state.goalBlockStreak += 1;
|
|
388
|
-
if (state.goalBlockStreak < config.goalBlockedWrapupTurns) {
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
state.goalBlockStreak = 0; // one assessment per streak; declines follow the fate cooldown
|
|
392
|
-
const logger = ctx.logger("continual-evolve");
|
|
393
|
-
logger.info(`auto-review goal-blocked trigger [${agent.id}]: ${config.goalBlockedWrapupTurns} consecutive blocked gate runs → local-fate assessment`);
|
|
394
|
-
await runLocalFatePhase(ctx, engine, agent, config, state, "goal_blocked", record);
|
|
395
|
-
}
|
|
396
|
-
/**
|
|
397
|
-
* Dedicated ZCode-style memory phase. It shares the scheduler snapshot and
|
|
398
|
-
* snapshot with the general review, but owns its request loop, manifest and
|
|
399
|
-
* memory-only tool policy. Its phase-specific checkpoint advances after a
|
|
400
|
-
* successful/no-op memory outcome; later review/fate failures do not replay
|
|
401
|
-
* old memory decisions, while memory failures leave the boundary retryable.
|
|
402
|
-
*/
|
|
403
|
-
export async function runMemoryExtractionPhase(ctx, engine, agent, config, state, snapshot, record, signal) {
|
|
404
|
-
const sessionId = agent.id;
|
|
405
|
-
const turnsSinceLastReview = state.turns - state.lastReviewAt;
|
|
406
|
-
const startedAt = Date.now();
|
|
407
|
-
const memorySnapshot = state.memoryCheckpoint ? sliceTurnSnapshot(snapshot, state.memoryCheckpoint) : snapshot;
|
|
408
|
-
if (!memorySnapshot.eligible || !memorySnapshot.trajectory) {
|
|
409
|
-
advanceMemoryCheckpoint(state, snapshot.cursor);
|
|
410
|
-
ctx.logger("continual-evolve").info(`memory agent no-op (${snapshot.reason}) [${agent.id}]: no new eligible evidence after checkpoint`);
|
|
411
|
-
record({
|
|
412
|
-
sessionId,
|
|
413
|
-
reason: snapshot.reason,
|
|
414
|
-
turnsSinceLastReview,
|
|
415
|
-
outcome: "noop",
|
|
416
|
-
rationale: `memory no-op (${snapshot.reason}): no new eligible evidence after checkpoint`,
|
|
417
|
-
durationMs: Date.now() - startedAt,
|
|
418
|
-
memoryTurns: 0,
|
|
419
|
-
memorySearches: 0,
|
|
420
|
-
appliedEdits: 0,
|
|
421
|
-
});
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
const declinedHit = matchDeclinedCheckpoint(loadDeclinedMemory(engine.baseDir), tokenize(memorySnapshot.trajectory));
|
|
425
|
-
if (declinedHit !== undefined) {
|
|
426
|
-
advanceMemoryCheckpoint(state, memorySnapshot.cursor);
|
|
427
|
-
ctx.logger("continual-evolve").info(`memory agent pre-suppressed (${snapshot.reason}) [${agent.id}]: checkpoint overlaps declined ${declinedHit.entry.scope}:${declinedHit.entry.fingerprint} (coverage ${(declinedHit.coverage * 100).toFixed(0)}%, ${declinedHit.hits} tokens) — agent not invoked`);
|
|
428
|
-
record({
|
|
429
|
-
sessionId,
|
|
430
|
-
reason: snapshot.reason,
|
|
431
|
-
turnsSinceLastReview,
|
|
432
|
-
outcome: "noop",
|
|
433
|
-
rationale: `memory pre-suppressed (${snapshot.reason}): checkpoint overlaps declined ${declinedHit.entry.scope}:${declinedHit.entry.fingerprint} — agent not invoked`,
|
|
434
|
-
durationMs: Date.now() - startedAt,
|
|
435
|
-
memoryTurns: 0,
|
|
436
|
-
memorySearches: 0,
|
|
437
|
-
appliedEdits: 0,
|
|
438
|
-
});
|
|
439
|
-
return;
|
|
440
|
-
}
|
|
441
|
-
const projectKey = snapshot.projectKey ?? projectKeyOf(agent);
|
|
442
|
-
const baselines = {
|
|
443
|
-
local: engine.load("local", sessionId),
|
|
444
|
-
global: engine.load("global", undefined),
|
|
445
|
-
...(projectKey ? { project: engine.load("project", projectKey) } : {}),
|
|
446
|
-
};
|
|
447
|
-
const manifest = buildMemoryManifest(loadGateHarnessView(engine, sessionId, projectKey ? { projectKey } : undefined));
|
|
448
|
-
const overrideRoute = parseReviewModel(config.reviewModel, agent.options.provider);
|
|
449
|
-
const provider = overrideRoute?.provider ?? agent.options.provider;
|
|
450
|
-
const model = overrideRoute?.model ?? agent.options.model;
|
|
451
|
-
if (!provider || !model)
|
|
452
|
-
throw new Error("evolve: no provider/model route for the memory extraction agent");
|
|
453
|
-
const tokenUsage = {
|
|
454
|
-
baseDir: engine.baseDir,
|
|
455
|
-
sessionId,
|
|
456
|
-
retain: engine.retention.tokenUsage,
|
|
457
|
-
onError: (cause) => ctx
|
|
458
|
-
.logger("continual-evolve")
|
|
459
|
-
.warn(`token-usage ledger failed for ${sessionId}: ${cause instanceof Error ? cause.message : String(cause)}`),
|
|
460
|
-
};
|
|
461
|
-
const run = await runMemoryAgent(ctx, {
|
|
462
|
-
provider,
|
|
463
|
-
model,
|
|
464
|
-
sessionId: asLlmSessionId(sessionId),
|
|
465
|
-
manifest,
|
|
466
|
-
trajectory: memorySnapshot.trajectory,
|
|
467
|
-
trajectoryEvents: memorySnapshot.events,
|
|
468
|
-
maxOutputTokens: config.budgetTokens,
|
|
469
|
-
...(signal ? { signal } : {}),
|
|
470
|
-
tokenUsage,
|
|
471
|
-
language: resolveRecordLanguage({ configured: config.recordLanguage, ctx, trajectoryText: memorySnapshot.trajectory }),
|
|
472
|
-
...((config.prefixCacheMode !== undefined || config.prefixMaxChars !== undefined
|
|
473
|
-
? {
|
|
474
|
-
prefixCache: {
|
|
475
|
-
...(config.prefixCacheMode !== undefined ? { mode: config.prefixCacheMode } : {}),
|
|
476
|
-
...(config.prefixMaxChars !== undefined ? { maxChars: config.prefixMaxChars } : {}),
|
|
477
|
-
},
|
|
478
|
-
}
|
|
479
|
-
: {})),
|
|
480
|
-
});
|
|
481
|
-
if (run.proposal.edits.length === 0) {
|
|
482
|
-
advanceMemoryCheckpoint(state, memorySnapshot.cursor);
|
|
483
|
-
ctx.logger("continual-evolve").info(`memory agent no-op (${snapshot.reason}) [${sessionId}] after ${run.turns} turn(s): ${run.proposal.rationale}`);
|
|
484
|
-
record({
|
|
485
|
-
sessionId,
|
|
486
|
-
reason: snapshot.reason,
|
|
487
|
-
turnsSinceLastReview,
|
|
488
|
-
outcome: "noop",
|
|
489
|
-
rationale: `memory no-op (${snapshot.reason}) after ${run.turns} turn(s): ${run.proposal.rationale}`,
|
|
490
|
-
durationMs: Date.now() - startedAt,
|
|
491
|
-
memoryTurns: run.turns,
|
|
492
|
-
memorySearches: run.searches,
|
|
493
|
-
appliedEdits: 0,
|
|
494
|
-
});
|
|
495
|
-
return;
|
|
496
|
-
}
|
|
497
|
-
const source = { sessionId, ...(memorySnapshot.sourceSeqs.length > 0 ? { seqs: [...memorySnapshot.sourceSeqs] } : {}) };
|
|
498
|
-
const application = await applyMemoryExtractionProposal(ctx, engine, run.proposal, {
|
|
499
|
-
agent,
|
|
500
|
-
baselines,
|
|
501
|
-
...(projectKey ? { projectKey } : {}),
|
|
502
|
-
requireApproval: config.requireGlobalApproval ?? true,
|
|
503
|
-
source,
|
|
504
|
-
decisionCursor: memorySnapshot.cursor,
|
|
505
|
-
scopeDecisions: state.memoryDecisions,
|
|
506
|
-
onScopeDecision: (key, decision) => rememberMemoryDecision(state, key, decision),
|
|
507
|
-
...(config.recordLanguage !== undefined ? { recordLanguage: config.recordLanguage } : {}),
|
|
508
|
-
...(signal ? { signal } : {}),
|
|
509
|
-
});
|
|
510
|
-
for (const result of application.results) {
|
|
511
|
-
emitEvolveComplete(engine.baseDir, buildEvolveCompleteEvent(result, `memory_agent:${snapshot.reason}`, sessionId), config.reviewsRetain ?? DEFAULT_REVIEWS_RETAIN);
|
|
512
|
-
}
|
|
513
|
-
state.memoryCheckpoint = memorySnapshot.cursor;
|
|
514
|
-
const applied = application.results.reduce((count, result) => count + result.appliedEdits.filter((edit) => edit.applied).length, 0);
|
|
515
|
-
const declined = application.declinedScopes.length > 0 ? `; declined scopes=${application.declinedScopes.join(",")}` : "";
|
|
516
|
-
const durationMs = Date.now() - startedAt;
|
|
517
|
-
for (const result of application.results) {
|
|
518
|
-
record({
|
|
519
|
-
sessionId,
|
|
520
|
-
reason: snapshot.reason,
|
|
521
|
-
turnsSinceLastReview,
|
|
522
|
-
outcome: "applied",
|
|
523
|
-
rationale: `memory applied (${snapshot.reason}) after ${run.turns} turn(s): ${run.proposal.rationale}${declined}`,
|
|
524
|
-
refinementId: result.id,
|
|
525
|
-
durationMs,
|
|
526
|
-
memoryTurns: run.turns,
|
|
527
|
-
memorySearches: run.searches,
|
|
528
|
-
appliedEdits: result.appliedEdits.filter((edit) => edit.applied).length,
|
|
529
|
-
});
|
|
530
|
-
}
|
|
531
|
-
if (application.declinedScopes.length > 0) {
|
|
532
|
-
record({
|
|
533
|
-
sessionId,
|
|
534
|
-
reason: snapshot.reason,
|
|
535
|
-
turnsSinceLastReview,
|
|
536
|
-
outcome: "declined",
|
|
537
|
-
rationale: `memory scopes declined (${snapshot.reason}): ${application.declinedScopes.join(",")}`,
|
|
538
|
-
durationMs,
|
|
539
|
-
memoryTurns: run.turns,
|
|
540
|
-
memorySearches: run.searches,
|
|
541
|
-
appliedEdits: 0,
|
|
542
|
-
});
|
|
543
|
-
}
|
|
544
|
-
ctx.logger("continual-evolve").info(`memory agent applied ${applied} edit(s) across ${application.results.length} scope batch(es) [${sessionId}] after ${run.turns} turn(s)${declined}: ${run.proposal.rationale}`);
|
|
545
|
-
// Visibility (P1 unified receipt): only applied outcomes wake the agent
|
|
546
|
-
// with a follow-up — and only on the turn path, never mid-compaction.
|
|
547
|
-
// No-op and declined-only runs stay audit-only (reviews.jsonl).
|
|
548
|
-
if (config.notifyOnAutoReview && snapshot.reason === "turn_snapshot" && applied > 0) {
|
|
549
|
-
notifyMemoryExtraction(ctx, agent, {
|
|
550
|
-
outcome: "applied",
|
|
551
|
-
results: application.results,
|
|
552
|
-
...(application.declinedScopes.length > 0 ? { declinedScopes: [...application.declinedScopes] } : {}),
|
|
553
|
-
turns: run.turns,
|
|
554
|
-
searches: run.searches,
|
|
555
|
-
durationMs,
|
|
556
|
-
});
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
async function runReviewPhase(ctx, engine, agent, config, state, snapshot, record, signal) {
|
|
560
|
-
const sessionId = agent.id;
|
|
561
|
-
const reason = snapshot.reason;
|
|
562
|
-
const turnsSinceLastReview = state.turns - state.lastReviewAt;
|
|
563
|
-
const logger = ctx.logger("continual-evolve");
|
|
564
|
-
const tokenUsage = {
|
|
565
|
-
baseDir: engine.baseDir,
|
|
566
|
-
sessionId,
|
|
567
|
-
retain: engine.retention.tokenUsage,
|
|
568
|
-
onError: (cause) => logger.warn(`token-usage ledger failed for ${sessionId}: ${cause instanceof Error ? cause.message : String(cause)}`),
|
|
569
|
-
};
|
|
570
|
-
const trajectory = snapshot.trajectory;
|
|
571
|
-
if (!trajectory) {
|
|
572
|
-
throw new Error("eligible snapshot has no trajectory");
|
|
573
|
-
}
|
|
574
|
-
// The gate judges the merged view (global + local, scopes labeled) so it
|
|
575
|
-
// can recognize topics already covered globally and decline duplicates;
|
|
576
|
-
// applying still targets the raw local store.
|
|
577
|
-
const localState = engine.load("local", sessionId);
|
|
578
|
-
const projectKey = projectKeyOf(agent);
|
|
579
|
-
const harnessState = loadGateHarnessView(engine, sessionId, projectKey ? { projectKey } : undefined);
|
|
580
|
-
const history = engine.history("local", sessionId);
|
|
581
|
-
// Gap C1: resolve optional review model override.
|
|
582
|
-
const reviewRoute = parseReviewModel(config.reviewModel, agent.options.provider);
|
|
583
|
-
// Records follow the client language: explicit config first, else the
|
|
584
|
-
// durable DSH preference, else this run's own trajectory.
|
|
585
|
-
const recordLanguage = resolveRecordLanguage({ configured: config.recordLanguage, ctx, trajectoryText: trajectory });
|
|
586
|
-
const review = await reviewAutoRefine(ctx, {
|
|
587
|
-
agent,
|
|
588
|
-
state: harnessState,
|
|
589
|
-
history,
|
|
590
|
-
trajectory,
|
|
591
|
-
trajectoryEvents: snapshot.events,
|
|
592
|
-
...(signal ? { signal } : {}),
|
|
593
|
-
context: { reason, turnsSinceLastReview },
|
|
594
|
-
budgetTokens: config.budgetTokens,
|
|
595
|
-
tokenUsage,
|
|
596
|
-
language: recordLanguage,
|
|
597
|
-
...(reviewRoute ? { overrideProvider: reviewRoute.provider, overrideModel: reviewRoute.model } : {}),
|
|
598
|
-
...((config.prefixCacheMode !== undefined || config.prefixMaxChars !== undefined
|
|
599
|
-
? {
|
|
600
|
-
prefixCache: {
|
|
601
|
-
...(config.prefixCacheMode !== undefined ? { mode: config.prefixCacheMode } : {}),
|
|
602
|
-
...(config.prefixMaxChars !== undefined ? { maxChars: config.prefixMaxChars } : {}),
|
|
603
|
-
},
|
|
604
|
-
}
|
|
605
|
-
: {})),
|
|
606
|
-
});
|
|
607
|
-
state.lastReviewAt = state.turns;
|
|
608
|
-
if (!review.shouldRefine) {
|
|
609
|
-
logger.info(`auto-review declined (${reason}) [${sessionId}] after ${turnsSinceLastReview} turns: ${review.rationale}`);
|
|
610
|
-
record({ sessionId, reason, turnsSinceLastReview, outcome: "declined", rationale: review.rationale });
|
|
611
|
-
return;
|
|
612
|
-
}
|
|
613
|
-
const plannedProposal = await planWithLlm(ctx, {
|
|
614
|
-
agent,
|
|
615
|
-
state: harnessState,
|
|
616
|
-
history,
|
|
617
|
-
trajectory: snapshot.trajectory,
|
|
618
|
-
trajectoryEvents: snapshot.events,
|
|
619
|
-
...(signal ? { signal } : {}),
|
|
620
|
-
...(review.instructions ? { instructions: review.instructions } : {}),
|
|
621
|
-
global: false,
|
|
622
|
-
// Read the skill-creator template facts (fallback: builtin distilled
|
|
623
|
-
// guide) so skill proposals follow the standard.
|
|
624
|
-
skillsRoot: join(engine.baseDir, "skills"),
|
|
625
|
-
tokenUsage,
|
|
626
|
-
language: recordLanguage,
|
|
627
|
-
...((config.prefixCacheMode !== undefined || config.prefixMaxChars !== undefined
|
|
628
|
-
? {
|
|
629
|
-
prefixCache: {
|
|
630
|
-
...(config.prefixCacheMode !== undefined ? { mode: config.prefixCacheMode } : {}),
|
|
631
|
-
...(config.prefixMaxChars !== undefined ? { maxChars: config.prefixMaxChars } : {}),
|
|
632
|
-
},
|
|
633
|
-
}
|
|
634
|
-
: {})),
|
|
635
|
-
});
|
|
636
|
-
const proposal = stripDedicatedMemoryEdits(plannedProposal);
|
|
637
|
-
const memoryOnlyPlan = isDedicatedMemoryOnlyProposal(plannedProposal);
|
|
638
|
-
// Skills are governed resources: an auto-created skill is OFFERED to the
|
|
639
|
-
// user for a decision (固化/不固化) before it lands — the gate never
|
|
640
|
-
// writes a skill silently. Without consent the skill edits are withheld
|
|
641
|
-
// and the rest of the proposal proceeds as usual.
|
|
642
|
-
const { skillEdits, otherEdits } = splitSkillEdits(proposal);
|
|
643
|
-
const skillConsented = await consultSkillEdits(ctx, agent, skillEdits, state, recordLanguage);
|
|
644
|
-
const finalProposal = skillConsented
|
|
645
|
-
? proposal
|
|
646
|
-
: {
|
|
647
|
-
...proposal,
|
|
648
|
-
edits: otherEdits,
|
|
649
|
-
summary: skillEdits.length > 0 ? `${proposal.summary} (skill edits withheld — pending user decision)` : proposal.summary,
|
|
650
|
-
};
|
|
651
|
-
if (finalProposal.edits.length === 0) {
|
|
652
|
-
const withheld = skillEdits.length > 0 ? " (skill proposal withheld — user not consulted or declined)" : "";
|
|
653
|
-
logger.info(`auto-review declined (${reason}) [${sessionId}]: no consented edits${withheld} — ${review.rationale}`);
|
|
654
|
-
// P1 auto-case capture: an attempted evolution that never landed is a
|
|
655
|
-
// regression asset. Contained — capture failure must not disturb the gate.
|
|
656
|
-
if (config.autoCase && !memoryOnlyPlan) {
|
|
657
|
-
try {
|
|
658
|
-
const captured = captureAutoCase({
|
|
659
|
-
baseDir: engine.baseDir,
|
|
660
|
-
rubricKey: config.rubricKey,
|
|
661
|
-
source: "gate_no_consent",
|
|
662
|
-
sessionId,
|
|
663
|
-
summary: finalProposal.summary,
|
|
664
|
-
reasons: [review.rationale],
|
|
665
|
-
});
|
|
666
|
-
logger.info(`auto-review auto-case captured (${reason}) [${sessionId}]: ${captured.caseId}`);
|
|
667
|
-
}
|
|
668
|
-
catch (cause) {
|
|
669
|
-
logger.warn(`auto-case capture failed for ${sessionId}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
record({ sessionId, reason, turnsSinceLastReview, outcome: "declined", rationale: `${review.rationale}${withheld}` });
|
|
673
|
-
return;
|
|
674
|
-
}
|
|
675
|
-
const source = { sessionId, ...(snapshot.sourceSeqs.length > 0 ? { seqs: [...snapshot.sourceSeqs] } : {}) };
|
|
676
|
-
const result = engine.apply("local", sessionId, finalProposal, {
|
|
677
|
-
scope: "local",
|
|
678
|
-
baselineState: localState,
|
|
679
|
-
...(source ? { source } : {}),
|
|
680
|
-
});
|
|
681
|
-
logger.info(`auto-review approved (${reason}) [${sessionId}] after ${turnsSinceLastReview} turns; auto-refine ${result.id}: ${result.appliedEdits.filter((e) => e.applied).length} applied, ${result.appliedEdits.filter((e) => !e.applied).length} failed — ${review.rationale}`);
|
|
682
|
-
record({ sessionId, reason, turnsSinceLastReview, outcome: "approved", rationale: review.rationale, refinementId: result.id });
|
|
683
|
-
// Gap C4: emit structured evolve_complete event for third-party consumers.
|
|
684
|
-
emitEvolveComplete(engine.baseDir, buildEvolveCompleteEvent(result, `auto_review:${reason}`, sessionId), config.reviewsRetain ?? DEFAULT_REVIEWS_RETAIN);
|
|
685
|
-
// Visibility: tell the user what the gate just persisted. Only the
|
|
686
|
-
// turn snapshot path notifies — a compaction-triggered gate must not wake
|
|
687
|
-
// the agent mid-compaction — and only when something was actually applied
|
|
688
|
-
// (a notice for zero edits is noise). Failure is contained in notifyAutoReview.
|
|
689
|
-
if (config.notifyOnAutoReview && reason === "turn_snapshot" && result.appliedEdits.some((e) => e.applied)) {
|
|
690
|
-
notifyAutoReview(ctx, agent, result, turnsSinceLastReview);
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
/** True when the general planner returned only edits owned by the memory phase. */
|
|
694
|
-
export function isDedicatedMemoryOnlyProposal(proposal) {
|
|
695
|
-
return proposal.edits.length > 0 && proposal.edits.every((edit) => edit.kind === "memory");
|
|
696
|
-
}
|
|
697
|
-
/**
|
|
698
|
-
* Remove memory edits from the general auto-review planner. The dedicated
|
|
699
|
-
* memory phase owns this responsibility and has already run against the same
|
|
700
|
-
* snapshot; accepting a second memory proposal would reintroduce duplicates.
|
|
701
|
-
*/
|
|
702
|
-
export function stripDedicatedMemoryEdits(proposal) {
|
|
703
|
-
const edits = proposal.edits.filter((edit) => edit.kind !== "memory");
|
|
704
|
-
if (edits.length === proposal.edits.length)
|
|
705
|
-
return proposal;
|
|
706
|
-
return {
|
|
707
|
-
...proposal,
|
|
708
|
-
edits,
|
|
709
|
-
summary: `${proposal.summary} (memory edits owned by dedicated extractor)`,
|
|
710
|
-
};
|
|
711
|
-
}
|
|
712
|
-
/**
|
|
713
|
-
* Split a proposal into skill edits and everything else. Skill edits are the
|
|
714
|
-
* governed part: they need explicit user consent before the gate applies
|
|
715
|
-
* them, while the remaining edits flow through the normal auto path.
|
|
716
|
-
*/
|
|
717
|
-
export function splitSkillEdits(proposal) {
|
|
718
|
-
return {
|
|
719
|
-
skillEdits: proposal.edits.filter((edit) => edit.kind === "skill"),
|
|
720
|
-
otherEdits: proposal.edits.filter((edit) => edit.kind !== "skill"),
|
|
721
|
-
};
|
|
722
|
-
}
|
|
723
|
-
/**
|
|
724
|
-
* Ask the user whether to solidify proposed skill edits (guidance or
|
|
725
|
-
* executable) into the harness. Returns true when every skill edit is
|
|
726
|
-
* consented. Never writes a skill silently:
|
|
727
|
-
* - no question service available → false (conservative);
|
|
728
|
-
* - the same candidate was rejected within the cooldown window → false
|
|
729
|
-
* without asking again (no nagging);
|
|
730
|
-
* - the user declines → false and the rejection is recorded for cooldown;
|
|
731
|
-
* - the question call fails/aborts → false (conservative).
|
|
732
|
-
*/
|
|
733
|
-
export async function consultSkillEdits(ctx, agent, skillEdits, gate, language) {
|
|
734
|
-
if (skillEdits.length === 0)
|
|
735
|
-
return true;
|
|
736
|
-
const key = skillEdits.map((edit) => edit.id ?? slug(edit.title ?? edit.kind, edit.kind)).join("|");
|
|
737
|
-
const lastReject = gate.skillRejects.get(key);
|
|
738
|
-
if (lastReject !== undefined && gate.turns - lastReject < SKILL_CONSULT_COOLDOWN_TURNS) {
|
|
739
|
-
return false;
|
|
740
|
-
}
|
|
741
|
-
const userQuestions = questionServiceOf(ctx);
|
|
742
|
-
if (!userQuestions) {
|
|
743
|
-
return false;
|
|
744
|
-
}
|
|
745
|
-
const lang = language ?? resolveDialogLanguage(ctx);
|
|
746
|
-
const description = skillEdits
|
|
747
|
-
.map((edit) => skillEditLine(edit.action, edit.title ?? edit.id ?? edit.kind, edit.skill_kind, lang))
|
|
748
|
-
.join("\n");
|
|
749
|
-
try {
|
|
750
|
-
const copy = skillConsultCopy(description, lang);
|
|
751
|
-
const answer = await userQuestions.ask({
|
|
752
|
-
questions: [
|
|
753
|
-
{
|
|
754
|
-
id: "evolve-skill-consult",
|
|
755
|
-
question: copy.question,
|
|
756
|
-
options: copy.options,
|
|
757
|
-
},
|
|
758
|
-
],
|
|
759
|
-
agent,
|
|
760
|
-
});
|
|
761
|
-
const item = answer.answers?.find((entry) => entry.id === "evolve-skill-consult");
|
|
762
|
-
const selected = item?.selected ?? [];
|
|
763
|
-
const consented = selected.includes("固化") || selected.includes("Solidify");
|
|
764
|
-
if (!consented) {
|
|
765
|
-
gate.skillRejects.set(key, gate.turns);
|
|
766
|
-
}
|
|
767
|
-
return consented;
|
|
768
|
-
}
|
|
769
|
-
catch {
|
|
770
|
-
return false;
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
//# sourceMappingURL=auto.js.map
|