dsh-continual-evolve 0.7.1 → 0.7.2
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 +13 -6
- package/README.zh.md +8 -6
- package/lib/auto.d.ts +11 -5
- package/lib/auto.js +119 -81
- 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 +27 -24
- package/lib/planner.d.ts +5 -0
- package/lib/planner.js +1 -1
- package/lib/review-scheduler.d.ts +37 -0
- package/lib/review-scheduler.js +110 -0
- package/lib/review.d.ts +3 -1
- package/lib/review.js +1 -1
- package/lib/runtime.d.ts +13 -13
- package/lib/runtime.js +41 -37
- package/lib/turn-snapshot.d.ts +58 -0
- package/lib/turn-snapshot.js +188 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
[](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
[](package.json)
|
|
10
|
-
[]()
|
|
11
11
|
|
|
12
12
|
Continual self-evolution for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness): a versioned, auditable, rollback-safe harness state layer — prompt notes, memories, skills, subagent specs — refined from session trajectories.
|
|
13
13
|
|
|
@@ -25,7 +25,7 @@ Agents accumulate reusable experience (repeated failures, durable facts, reusabl
|
|
|
25
25
|
|
|
26
26
|
## How it works
|
|
27
27
|
|
|
28
|
-
1. **Sediment** — the model creates entries via `evolve_add`, or the automatic review gate proposes them from
|
|
28
|
+
1. **Sediment** — the model creates entries via `evolve_add`, or the automatic review gate proposes them from incremental snapshots captured after successful turns (plus compaction checkpoints).
|
|
29
29
|
2. **Guard** — code-enforced validation: edit schema, blast-radius/scope coherence, and the promotion policy (project-scoped markers, thin content, near-duplicate detection, credential screening keep the global store clean — secrets are rejected at every write sink, including mount materialization). Global creates that near-duplicate an existing entry are rejected at write time (≥0.8 similarity); moderate overlaps carry a `conflictHint` for later consolidation.
|
|
30
30
|
3. **Approve** — global and project writes require explicit human approval; local-fate proposals are consulted before they land.
|
|
31
31
|
4. **Apply & inject** — atomic apply with snapshot + audit event. Prompt notes and delegation specs inject into the system prompt (capped, relevance-ranked, contradicted entries demoted, zero tokens when empty); memories/skills appear as a relevance-ordered capped directory index (`- [memory:type:id] title` hooks, full text one `evolve_list` away).
|
|
@@ -77,8 +77,8 @@ Injection shape: prompt notes and delegation specs inject with content (≤6/kin
|
|
|
77
77
|
| Key | Default | Meaning |
|
|
78
78
|
|---|---|---|
|
|
79
79
|
| `baseDir` | resolved DSH home | root for the `evolve/` stores |
|
|
80
|
-
| `autoReview` | `false` |
|
|
81
|
-
| `reviewIntervalTurns` | `6` |
|
|
80
|
+
| `autoReview` | `false` | initial automatic-review default when no runtime switch exists; the listener is always registered |
|
|
81
|
+
| `reviewIntervalTurns` | `6` | legacy local-fate cadence fallback; successful-turn review no longer waits for this interval |
|
|
82
82
|
| `maxReviewInputChars` | `40000` | trajectory slice handed to the gate |
|
|
83
83
|
| `reviewBudgetTokens` | `4096` | output budget for the gate call |
|
|
84
84
|
| `notifyOnAutoReview` | `true` | visible follow-up notice after an applied gate run |
|
|
@@ -109,11 +109,18 @@ Example profile patch:
|
|
|
109
109
|
reviewIntervalTurns: 6
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
+
The listener is registered even when `autoReview` is `false`. Use `/evolve resume`
|
|
113
|
+
to enable successful-turn snapshots immediately, `/evolve pause` to suppress new
|
|
114
|
+
snapshots and fate/model work, and `/evolve status` to inspect the configured
|
|
115
|
+
default plus the current runtime state. The runtime switch is stored in
|
|
116
|
+
`evolve/runtime.json`; manual `evolve_*` tools and `/evolve` commands are not
|
|
117
|
+
paused.
|
|
118
|
+
|
|
112
119
|
## Development
|
|
113
120
|
|
|
114
121
|
```bash
|
|
115
122
|
pnpm install && pnpm build # deps + tsc -> lib/
|
|
116
|
-
pnpm test # vitest (
|
|
123
|
+
pnpm test # vitest (675 tests)
|
|
117
124
|
pnpm test:coverage # v8 coverage, thresholds enforced in CI
|
|
118
125
|
pnpm lint # oxlint src test
|
|
119
126
|
```
|
|
@@ -122,7 +129,7 @@ Project layout:
|
|
|
122
129
|
|
|
123
130
|
```
|
|
124
131
|
├── src/ # engine, tools, commands, gate, fate, benchmark, injection + token usage…
|
|
125
|
-
├── test/ # vitest suites (
|
|
132
|
+
├── test/ # vitest suites (43 files)
|
|
126
133
|
├── lib/ # build output (tsc)
|
|
127
134
|
├── docs/
|
|
128
135
|
│ ├── design.md # full design doc (hardening matrix)
|
package/README.zh.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
[](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
[](package.json)
|
|
10
|
-
[]()
|
|
11
11
|
|
|
12
12
|
[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(`dsh`)的持续自进化插件:一套**版本化、可审计、可回滚**的 harness 状态层——提示词补充、记忆、技能、子代理规格——从会话轨迹中沉淀而来。
|
|
13
13
|
|
|
@@ -25,7 +25,7 @@ Agent 在每个会话里积累可复用经验(重复失败、持久事实、
|
|
|
25
25
|
|
|
26
26
|
## 工作原理
|
|
27
27
|
|
|
28
|
-
1. **沉淀**——模型经 `evolve_add` 创建条目,或自动 review
|
|
28
|
+
1. **沉淀**——模型经 `evolve_add` 创建条目,或自动 review 门禁从成功回合后的增量 snapshot(以及压缩检查点)提议。
|
|
29
29
|
2. **守卫**——代码强制校验:编辑 schema、blast-radius 与作用域一致性、晋升政策(项目专属标记 / 过薄内容 / 近似重复检测 / 凭据筛查保持全局库干净——密钥类内容在所有写入出口被拒,含 mount 物化)。全局 create 与既有条目高度相似(≥0.8)时写入即拒;中等重叠带 `conflictHint` 供后续合并。
|
|
30
30
|
3. **审批**——全局与项目写入需明确人工批准;local 归宿提议先征询后落地。
|
|
31
31
|
4. **应用与注入**——原子应用带快照与审计事件。prompt 补充与委派规格注入系统提示词(封顶、按相关性排序、被证伪条目降权、空 store 零 token);memory/skill 以按相关性排序的目录索引出现(`[memory:type:id] 标题`钩子)。
|
|
@@ -77,8 +77,8 @@ dsh plugin add ZK-Andy/dsh-continual-evolve
|
|
|
77
77
|
| 键 | 默认 | 含义 |
|
|
78
78
|
|---|---|---|
|
|
79
79
|
| `baseDir` | 解析后的 DSH home | `evolve/` 存储根目录 |
|
|
80
|
-
| `autoReview` | `false` |
|
|
81
|
-
| `reviewIntervalTurns` | `6` |
|
|
80
|
+
| `autoReview` | `false` | 没有 runtime 开关时的自动 review 初始默认;监听器始终注册 |
|
|
81
|
+
| `reviewIntervalTurns` | `6` | local-fate 的兼容节奏;成功回合 review 不再等待这个间隔 |
|
|
82
82
|
| `maxReviewInputChars` | `40000` | 交给门禁的轨迹切片 |
|
|
83
83
|
| `reviewBudgetTokens` | `4096` | 门禁调用输出预算 |
|
|
84
84
|
| `notifyOnAutoReview` | `true` | 门禁应用后发可见跟进通知 |
|
|
@@ -109,11 +109,13 @@ profile patch 示例:
|
|
|
109
109
|
reviewIntervalTurns: 6
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
+
即使 `autoReview` 为 `false`,监听器也会注册。使用 `/evolve resume` 立即开启成功回合 snapshot,使用 `/evolve pause` 抑制新 snapshot、fate 和模型调用,使用 `/evolve status` 查看配置默认值与运行时状态。开关保存在 `evolve/runtime.json`;手动 `evolve_*` 工具和 `/evolve` 命令不受暂停影响。
|
|
113
|
+
|
|
112
114
|
## 开发
|
|
113
115
|
|
|
114
116
|
```bash
|
|
115
117
|
pnpm install && pnpm build # 依赖 + tsc -> lib/
|
|
116
|
-
pnpm test # vitest(
|
|
118
|
+
pnpm test # vitest(675 例)
|
|
117
119
|
pnpm test:coverage # v8 覆盖率,CI 强制阈值
|
|
118
120
|
pnpm lint # oxlint src test
|
|
119
121
|
```
|
|
@@ -122,7 +124,7 @@ pnpm lint # oxlint src test
|
|
|
122
124
|
|
|
123
125
|
```
|
|
124
126
|
├── src/ # 引擎、工具、命令、门禁、fate、benchmark、注入 + token 用量…
|
|
125
|
-
├── test/ # vitest 测试套件(
|
|
127
|
+
├── test/ # vitest 测试套件(43 个文件)
|
|
126
128
|
├── lib/ # 构建产物(tsc)
|
|
127
129
|
├── docs/
|
|
128
130
|
│ ├── design.md # 完整设计文档(硬化矩阵)
|
package/lib/auto.d.ts
CHANGED
|
@@ -6,7 +6,10 @@ import { type AutoRefineReason } from "./review.js";
|
|
|
6
6
|
import type { PromotionPolicy } from "./promotion.js";
|
|
7
7
|
import type { PlannerPrefixCacheMode } from "./prefix-cache.js";
|
|
8
8
|
export interface AutoReviewConfig {
|
|
9
|
+
/** Legacy fate cadence; successful turns are no longer gated by this value. */
|
|
9
10
|
intervalTurns: number;
|
|
11
|
+
/** Initial runtime state when no runtime.json exists. */
|
|
12
|
+
enabledByDefault?: boolean;
|
|
10
13
|
maxInputChars: number;
|
|
11
14
|
budgetTokens: number;
|
|
12
15
|
/** Queue a visible follow-up notice after an approved, applied gate run. */
|
|
@@ -18,10 +21,9 @@ export interface AutoReviewConfig {
|
|
|
18
21
|
*/
|
|
19
22
|
localFate: boolean;
|
|
20
23
|
/**
|
|
21
|
-
* Minimum turns between local-fate assessments on the turn
|
|
22
|
-
* (compaction is unconditional). Independent of
|
|
23
|
-
*
|
|
24
|
-
* round.
|
|
24
|
+
* Minimum turns between local-fate assessments on the successful-turn path
|
|
25
|
+
* (compaction is unconditional). Independent of snapshot review cadence so
|
|
26
|
+
* eligible turns do not pay an assessment every round.
|
|
25
27
|
*/
|
|
26
28
|
fateIntervalTurns: number;
|
|
27
29
|
/**
|
|
@@ -68,6 +70,10 @@ export interface AutoReviewConfig {
|
|
|
68
70
|
}
|
|
69
71
|
export interface GateState {
|
|
70
72
|
turns: number;
|
|
73
|
+
/** Latest successful turn boundary seen by the listener. */
|
|
74
|
+
completedTurn: number;
|
|
75
|
+
/** Latest turn whose snapshot acquisition was reserved. */
|
|
76
|
+
lastSnapshotTurn: number;
|
|
71
77
|
lastReviewAt: number;
|
|
72
78
|
running: boolean;
|
|
73
79
|
/**
|
|
@@ -98,7 +104,7 @@ export interface ReviewRecord {
|
|
|
98
104
|
sessionId: string;
|
|
99
105
|
reason: AutoRefineReason;
|
|
100
106
|
turnsSinceLastReview: number;
|
|
101
|
-
outcome: "approved" | "declined" | "failed" | "assessed" | "deferred";
|
|
107
|
+
outcome: "approved" | "declined" | "failed" | "assessed" | "deferred" | "skipped" | "armed";
|
|
102
108
|
rationale?: string;
|
|
103
109
|
refinementId?: string;
|
|
104
110
|
}
|
package/lib/auto.js
CHANGED
|
@@ -5,38 +5,42 @@
|
|
|
5
5
|
* fire-and-forget with error containment: an auto-review failure never
|
|
6
6
|
* disturbs the agent loop.
|
|
7
7
|
*
|
|
8
|
-
* Every gate decision (approved / declined / failed) is appended to
|
|
8
|
+
* Every gate decision (approved / declined / failed / skipped) is appended to
|
|
9
9
|
* `<dshHome>/evolve/reviews.jsonl` so auto-review activity is durably
|
|
10
10
|
* auditable — the server console is not a reliable place to look.
|
|
11
11
|
*
|
|
12
12
|
* Hook wiring:
|
|
13
|
-
* - `agent/turn-stopping`
|
|
14
|
-
* - `agent/status` (idle)
|
|
15
|
-
*
|
|
16
|
-
*
|
|
13
|
+
* - `agent/turn-stopping` records the successful turn boundary.
|
|
14
|
+
* - `agent/status` (idle) captures an incremental snapshot and feeds the
|
|
15
|
+
* per-session latest-pending serial scheduler.
|
|
16
|
+
* - `session/event` (compaction/start) forces a snapshot flush before data is
|
|
17
|
+
* summarized away.
|
|
17
18
|
*/
|
|
18
19
|
import { appendFileSync, mkdirSync } from "node:fs";
|
|
19
20
|
import { join } from "node:path";
|
|
20
21
|
import { slug } from "./types.js";
|
|
21
22
|
import { planWithLlm } from "./planner.js";
|
|
22
|
-
import { reviewAutoRefine
|
|
23
|
-
import {
|
|
23
|
+
import { reviewAutoRefine } from "./review.js";
|
|
24
|
+
import { goalServiceOf } from "./goal.js";
|
|
24
25
|
import { notifyAutoReview } from "./notify.js";
|
|
25
26
|
import { runLocalFatePhase } from "./fate.js";
|
|
26
|
-
import { entrySourceOf } from "./source.js";
|
|
27
27
|
import { mergeHarnessStates } from "./state.js";
|
|
28
28
|
import { projectKeyOf } from "./project.js";
|
|
29
29
|
import { questionServiceOf } from "./approval.js";
|
|
30
30
|
import { buildEvolveCompleteEvent, emitEvolveComplete } from "./evolve-event.js";
|
|
31
31
|
import { DEFAULT_REVIEWS_RETAIN, pruneJsonlFile } from "./store.js";
|
|
32
|
-
import {
|
|
32
|
+
import { isGateEnabled } from "./runtime.js";
|
|
33
|
+
import { createReviewScheduler } from "./review-scheduler.js";
|
|
34
|
+
import { captureTurnSnapshot } from "./turn-snapshot.js";
|
|
33
35
|
import { captureAutoCase } from "./autocase.js";
|
|
34
36
|
/** Turns a rejected skill candidate stays silent before being offered again. */
|
|
35
37
|
export const SKILL_CONSULT_COOLDOWN_TURNS = 10;
|
|
36
38
|
export function registerAutoReview(ctx, engine, config) {
|
|
37
39
|
const perSession = new Map();
|
|
40
|
+
const schedulers = new Map();
|
|
38
41
|
const logger = ctx.logger("continual-evolve");
|
|
39
42
|
const reviewsPath = join(engine.baseDir, "evolve", "reviews.jsonl");
|
|
43
|
+
const defaultEnabled = config.enabledByDefault ?? true;
|
|
40
44
|
const record = (entry) => {
|
|
41
45
|
try {
|
|
42
46
|
mkdirSync(join(engine.baseDir, "evolve"), { recursive: true });
|
|
@@ -54,9 +58,77 @@ export function registerAutoReview(ctx, engine, config) {
|
|
|
54
58
|
// ignored — the next record retries
|
|
55
59
|
}
|
|
56
60
|
};
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
const schedulerFor = (agent, state) => {
|
|
62
|
+
const existing = schedulers.get(agent.id);
|
|
63
|
+
if (existing)
|
|
64
|
+
return existing;
|
|
65
|
+
const scheduler = createReviewScheduler(async ({ snapshot, signal }) => {
|
|
66
|
+
if (!isGateEnabled(engine.baseDir, defaultEnabled))
|
|
67
|
+
return "aborted";
|
|
68
|
+
if (!snapshot.eligible) {
|
|
69
|
+
record({
|
|
70
|
+
sessionId: snapshot.sessionId,
|
|
71
|
+
reason: snapshot.reason,
|
|
72
|
+
turnsSinceLastReview: state.turns - state.lastReviewAt,
|
|
73
|
+
outcome: "skipped",
|
|
74
|
+
rationale: `snapshot ${snapshot.cursor} skipped: ${snapshot.skipReason ?? "not eligible"}`,
|
|
75
|
+
});
|
|
76
|
+
return "no-op";
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
await runGate(ctx, engine, agent, config, state, snapshot, record, signal);
|
|
80
|
+
return "success";
|
|
81
|
+
}
|
|
82
|
+
catch (cause) {
|
|
83
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
84
|
+
logger.warn(`auto-review failed for ${agent.id}: ${message}`);
|
|
85
|
+
record({
|
|
86
|
+
sessionId: agent.id,
|
|
87
|
+
reason: snapshot.reason,
|
|
88
|
+
turnsSinceLastReview: state.turns - state.lastReviewAt,
|
|
89
|
+
outcome: "failed",
|
|
90
|
+
rationale: `gate error: ${message}`,
|
|
91
|
+
});
|
|
92
|
+
return "error";
|
|
93
|
+
}
|
|
94
|
+
}, (snapshot) => snapshot.cursor);
|
|
95
|
+
schedulers.set(agent.id, scheduler);
|
|
96
|
+
return scheduler;
|
|
97
|
+
};
|
|
98
|
+
const captureAndSchedule = (agent, state, reason) => {
|
|
99
|
+
if (!isGateEnabled(engine.baseDir, defaultEnabled))
|
|
100
|
+
return;
|
|
101
|
+
const turn = state.completedTurn;
|
|
102
|
+
const previousSnapshotTurn = state.lastSnapshotTurn;
|
|
103
|
+
if (reason === "turn_snapshot" && turn <= previousSnapshotTurn)
|
|
104
|
+
return;
|
|
105
|
+
if (reason === "turn_snapshot")
|
|
106
|
+
state.lastSnapshotTurn = turn;
|
|
107
|
+
const cursor = schedulerFor(agent, state).getCursor();
|
|
108
|
+
void captureTurnSnapshot(ctx, agent, {
|
|
109
|
+
turn,
|
|
110
|
+
reason,
|
|
111
|
+
...(cursor !== undefined ? { cursor } : {}),
|
|
112
|
+
maxChars: config.maxInputChars,
|
|
113
|
+
})
|
|
114
|
+
.then((snapshot) => schedulerFor(agent, state).schedule(snapshot))
|
|
115
|
+
.catch((cause) => {
|
|
116
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
117
|
+
logger.warn(`auto-review snapshot failed for ${agent.id}: ${message}`);
|
|
118
|
+
record({
|
|
119
|
+
sessionId: agent.id,
|
|
120
|
+
reason,
|
|
121
|
+
turnsSinceLastReview: state.turns - state.lastReviewAt,
|
|
122
|
+
outcome: "failed",
|
|
123
|
+
rationale: `snapshot error: ${message}`,
|
|
124
|
+
});
|
|
125
|
+
if (reason === "turn_snapshot" && state.lastSnapshotTurn === turn) {
|
|
126
|
+
state.lastSnapshotTurn = previousSnapshotTurn;
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
// DSH emits this awaited boundary when a turn is about to close. The idle
|
|
131
|
+
// transition is the safe point at which to read the durable surface.
|
|
60
132
|
ctx.on("agent/turn-stopping", (payload) => {
|
|
61
133
|
const agent = payload.agent;
|
|
62
134
|
if (!agent) {
|
|
@@ -65,42 +137,20 @@ export function registerAutoReview(ctx, engine, config) {
|
|
|
65
137
|
}
|
|
66
138
|
const state = stateFor(perSession, agent.id);
|
|
67
139
|
state.turns += 1;
|
|
140
|
+
if (typeof payload.turn === "number")
|
|
141
|
+
state.completedTurn = Math.max(state.completedTurn, payload.turn);
|
|
68
142
|
});
|
|
69
143
|
ctx.on("agent/status", (payload) => {
|
|
70
144
|
const agent = payload.agent;
|
|
71
145
|
if (!agent || payload.status !== "idle")
|
|
72
146
|
return;
|
|
73
147
|
const state = stateFor(perSession, agent.id);
|
|
74
|
-
|
|
75
|
-
// (the goal's round machine keeps the session continuing); without a
|
|
76
|
-
// goal the plain turn interval applies.
|
|
77
|
-
const goalDriven = goalDrivesRounds(goalServiceOf(ctx)?.get(agent));
|
|
78
|
-
if (!goalDriven && state.turns - state.lastReviewAt < config.intervalTurns)
|
|
79
|
-
return;
|
|
80
|
-
// #21 P2 runtime switch: a paused gate stays fully dormant — no LLM
|
|
81
|
-
// calls, no fate assessments, no audit records. Checked after the
|
|
82
|
-
// cheap in-memory interval check so the paused path costs one file
|
|
83
|
-
// read at most. Manual tools/commands are unaffected (separate path).
|
|
84
|
-
if (isGatePaused(engine.baseDir))
|
|
148
|
+
if (state.completedTurn <= state.lastSnapshotTurn)
|
|
85
149
|
return;
|
|
86
|
-
|
|
87
|
-
// Every failure is durably recorded — nothing fails silently.
|
|
88
|
-
void runGate(ctx, engine, agent, config, state, "turn_interval", record).catch((cause) => {
|
|
89
|
-
const message = cause instanceof Error ? cause.message : String(cause);
|
|
90
|
-
logger.warn(`auto-review failed for ${agent.id}: ${message}`);
|
|
91
|
-
record({
|
|
92
|
-
sessionId: agent.id,
|
|
93
|
-
reason: "turn_interval",
|
|
94
|
-
turnsSinceLastReview: state.turns - state.lastReviewAt,
|
|
95
|
-
outcome: "failed",
|
|
96
|
-
rationale: `gate error: ${message}`,
|
|
97
|
-
});
|
|
98
|
-
state.lastReviewAt = state.turns; // back off until the interval elapses again
|
|
99
|
-
});
|
|
150
|
+
captureAndSchedule(agent, state, "turn_snapshot");
|
|
100
151
|
});
|
|
101
|
-
// Diagnostic: the armed marker proves
|
|
102
|
-
//
|
|
103
|
-
// trigger events are not reaching this listener.
|
|
152
|
+
// Diagnostic: the armed marker proves the listener was registered even when
|
|
153
|
+
// its runtime default is off, distinguishing "not registered" from "off".
|
|
104
154
|
try {
|
|
105
155
|
mkdirSync(join(engine.baseDir, "evolve"), { recursive: true });
|
|
106
156
|
appendFileSync(reviewsPath, `${JSON.stringify({
|
|
@@ -109,7 +159,7 @@ export function registerAutoReview(ctx, engine, config) {
|
|
|
109
159
|
reason: "boot",
|
|
110
160
|
turnsSinceLastReview: 0,
|
|
111
161
|
outcome: "armed",
|
|
112
|
-
rationale: `auto-review gate registered (
|
|
162
|
+
rationale: `auto-review gate registered (default=${defaultEnabled ? "on" : "off"}; per-success-turn snapshots; local-fate every ${config.fateIntervalTurns} turns)`,
|
|
113
163
|
})}\n`, "utf8");
|
|
114
164
|
}
|
|
115
165
|
catch (cause) {
|
|
@@ -121,6 +171,14 @@ export function registerAutoReview(ctx, engine, config) {
|
|
|
121
171
|
catch {
|
|
122
172
|
// ignored — the next record retries
|
|
123
173
|
}
|
|
174
|
+
ctx.on("agent/disposed", (payload) => {
|
|
175
|
+
const agent = payload.agent;
|
|
176
|
+
if (!agent)
|
|
177
|
+
return;
|
|
178
|
+
schedulers.get(agent.id)?.shutdown();
|
|
179
|
+
schedulers.delete(agent.id);
|
|
180
|
+
perSession.delete(agent.id);
|
|
181
|
+
});
|
|
124
182
|
ctx.on("session/event", (session, event) => {
|
|
125
183
|
if (event.type !== "compaction/start")
|
|
126
184
|
return;
|
|
@@ -129,21 +187,7 @@ export function registerAutoReview(ctx, engine, config) {
|
|
|
129
187
|
if (!agent)
|
|
130
188
|
return; // no live agent for that session (e.g. cold read)
|
|
131
189
|
const state = stateFor(perSession, agent.id);
|
|
132
|
-
|
|
133
|
-
// (#21): a paused gate skips even the compaction run, so "pause"
|
|
134
|
-
// is a true zero-LLM-call state.
|
|
135
|
-
if (isGatePaused(engine.baseDir))
|
|
136
|
-
return;
|
|
137
|
-
void runGate(ctx, engine, agent, config, state, "compact", record).catch((cause) => {
|
|
138
|
-
logger.warn(`auto-review failed at compaction for ${agent.id}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
139
|
-
record({
|
|
140
|
-
sessionId: agent.id,
|
|
141
|
-
reason: "compact",
|
|
142
|
-
turnsSinceLastReview: state.turns - state.lastReviewAt,
|
|
143
|
-
outcome: "failed",
|
|
144
|
-
rationale: `gate error at compaction: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
145
|
-
});
|
|
146
|
-
});
|
|
190
|
+
captureAndSchedule(agent, state, "compact");
|
|
147
191
|
});
|
|
148
192
|
}
|
|
149
193
|
/**
|
|
@@ -167,6 +211,8 @@ function stateFor(map, sessionId) {
|
|
|
167
211
|
if (!state) {
|
|
168
212
|
state = {
|
|
169
213
|
turns: 0,
|
|
214
|
+
completedTurn: 0,
|
|
215
|
+
lastSnapshotTurn: 0,
|
|
170
216
|
lastReviewAt: 0,
|
|
171
217
|
running: false,
|
|
172
218
|
skillRejects: new Map(),
|
|
@@ -199,23 +245,22 @@ export function loadGateHarnessView(engine, sessionId, opts) {
|
|
|
199
245
|
* review's baseline fresh — fate re-loads the store and never races the
|
|
200
246
|
* review's optimistic-concurrency checks.
|
|
201
247
|
*/
|
|
202
|
-
async function runGate(ctx, engine, agent, config, state,
|
|
248
|
+
async function runGate(ctx, engine, agent, config, state, snapshot, record, signal) {
|
|
203
249
|
// Reentry guard (review audit 2026-08-28 S3): a gate run holds LLM calls
|
|
204
250
|
// and possibly a user question for a long time; an idle/compaction
|
|
205
251
|
// trigger overlapping the run would start a second concurrent pipeline
|
|
206
252
|
// whose stale whole-file saves clobber the first run's writes.
|
|
207
253
|
if (state.running) {
|
|
208
|
-
|
|
209
|
-
return;
|
|
254
|
+
throw new Error(`previous gate run still in flight for ${agent.id}`);
|
|
210
255
|
}
|
|
211
256
|
state.running = true;
|
|
212
257
|
try {
|
|
213
|
-
await runReviewPhase(ctx, engine, agent, config, state,
|
|
258
|
+
await runReviewPhase(ctx, engine, agent, config, state, snapshot, record, signal);
|
|
214
259
|
// D3: a goal stuck in "blocked" for consecutive gate runs gets one
|
|
215
260
|
// local-fate assessment (the pipeline below), so whatever led the
|
|
216
261
|
// goal astray is distilled before the session moves on.
|
|
217
|
-
await runGoalBlockedFate(ctx, engine, agent, config, state, reason, record);
|
|
218
|
-
await runLocalFatePhase(ctx, engine, agent, config, state, reason, record);
|
|
262
|
+
await runGoalBlockedFate(ctx, engine, agent, config, state, snapshot.reason, record);
|
|
263
|
+
await runLocalFatePhase(ctx, engine, agent, config, state, snapshot.reason, record);
|
|
219
264
|
}
|
|
220
265
|
finally {
|
|
221
266
|
state.running = false;
|
|
@@ -250,8 +295,9 @@ export async function runGoalBlockedFate(ctx, engine, agent, config, state, _rea
|
|
|
250
295
|
logger.info(`auto-review goal-blocked trigger [${agent.id}]: ${config.goalBlockedWrapupTurns} consecutive blocked gate runs → local-fate assessment`);
|
|
251
296
|
await runLocalFatePhase(ctx, engine, agent, config, state, "goal_blocked", record);
|
|
252
297
|
}
|
|
253
|
-
async function runReviewPhase(ctx, engine, agent, config, state,
|
|
298
|
+
async function runReviewPhase(ctx, engine, agent, config, state, snapshot, record, signal) {
|
|
254
299
|
const sessionId = agent.id;
|
|
300
|
+
const reason = snapshot.reason;
|
|
255
301
|
const turnsSinceLastReview = state.turns - state.lastReviewAt;
|
|
256
302
|
const logger = ctx.logger("continual-evolve");
|
|
257
303
|
const tokenUsage = {
|
|
@@ -260,14 +306,9 @@ async function runReviewPhase(ctx, engine, agent, config, state, reason, record)
|
|
|
260
306
|
retain: engine.retention.tokenUsage,
|
|
261
307
|
onError: (cause) => logger.warn(`token-usage ledger failed for ${sessionId}: ${cause instanceof Error ? cause.message : String(cause)}`),
|
|
262
308
|
};
|
|
263
|
-
const trajectory =
|
|
264
|
-
logger.warn(`auto-review skipped for ${sessionId}: trajectory unavailable: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
265
|
-
record({ sessionId, reason, turnsSinceLastReview, outcome: "failed", rationale: `trajectory unavailable: ${cause instanceof Error ? cause.message : String(cause)}` });
|
|
266
|
-
return undefined;
|
|
267
|
-
});
|
|
309
|
+
const trajectory = snapshot.trajectory;
|
|
268
310
|
if (!trajectory) {
|
|
269
|
-
|
|
270
|
-
return;
|
|
311
|
+
throw new Error("eligible snapshot has no trajectory");
|
|
271
312
|
}
|
|
272
313
|
// The gate judges the merged view (global + local, scopes labeled) so it
|
|
273
314
|
// can recognize topics already covered globally and decline duplicates;
|
|
@@ -283,6 +324,8 @@ async function runReviewPhase(ctx, engine, agent, config, state, reason, record)
|
|
|
283
324
|
state: harnessState,
|
|
284
325
|
history,
|
|
285
326
|
trajectory,
|
|
327
|
+
trajectoryEvents: snapshot.events,
|
|
328
|
+
...(signal ? { signal } : {}),
|
|
286
329
|
context: { reason, turnsSinceLastReview },
|
|
287
330
|
budgetTokens: config.budgetTokens,
|
|
288
331
|
tokenUsage,
|
|
@@ -306,6 +349,9 @@ async function runReviewPhase(ctx, engine, agent, config, state, reason, record)
|
|
|
306
349
|
agent,
|
|
307
350
|
state: harnessState,
|
|
308
351
|
history,
|
|
352
|
+
trajectory: snapshot.trajectory,
|
|
353
|
+
trajectoryEvents: snapshot.events,
|
|
354
|
+
...(signal ? { signal } : {}),
|
|
309
355
|
...(review.instructions ? { instructions: review.instructions } : {}),
|
|
310
356
|
global: false,
|
|
311
357
|
// Read the skill-creator template facts (fallback: builtin distilled
|
|
@@ -358,7 +404,7 @@ async function runReviewPhase(ctx, engine, agent, config, state, reason, record)
|
|
|
358
404
|
record({ sessionId, reason, turnsSinceLastReview, outcome: "declined", rationale: `${review.rationale}${withheld}` });
|
|
359
405
|
return;
|
|
360
406
|
}
|
|
361
|
-
const source =
|
|
407
|
+
const source = { sessionId, ...(snapshot.sourceSeqs.length > 0 ? { seqs: [...snapshot.sourceSeqs] } : {}) };
|
|
362
408
|
const result = engine.apply("local", sessionId, finalProposal, {
|
|
363
409
|
scope: "local",
|
|
364
410
|
baselineState: localState,
|
|
@@ -369,21 +415,13 @@ async function runReviewPhase(ctx, engine, agent, config, state, reason, record)
|
|
|
369
415
|
// Gap C4: emit structured evolve_complete event for third-party consumers.
|
|
370
416
|
emitEvolveComplete(engine.baseDir, buildEvolveCompleteEvent(result, `auto_review:${reason}`, sessionId), config.reviewsRetain ?? DEFAULT_REVIEWS_RETAIN);
|
|
371
417
|
// Visibility: tell the user what the gate just persisted. Only the
|
|
372
|
-
// turn
|
|
418
|
+
// turn snapshot path notifies — a compaction-triggered gate must not wake
|
|
373
419
|
// the agent mid-compaction — and only when something was actually applied
|
|
374
420
|
// (a notice for zero edits is noise). Failure is contained in notifyAutoReview.
|
|
375
|
-
if (config.notifyOnAutoReview && reason === "
|
|
421
|
+
if (config.notifyOnAutoReview && reason === "turn_snapshot" && result.appliedEdits.some((e) => e.applied)) {
|
|
376
422
|
notifyAutoReview(ctx, agent, result, turnsSinceLastReview);
|
|
377
423
|
}
|
|
378
424
|
}
|
|
379
|
-
async function readTrajectory(ctx, agent, maxChars) {
|
|
380
|
-
const sessionQuery = ctx.sessionQuery;
|
|
381
|
-
if (!sessionQuery) {
|
|
382
|
-
throw new Error("sessionQuery unavailable");
|
|
383
|
-
}
|
|
384
|
-
const snapshot = await sessionQuery.readSurface(agent.id);
|
|
385
|
-
return serializeSurface(snapshot.events, maxChars);
|
|
386
|
-
}
|
|
387
425
|
/**
|
|
388
426
|
* Split a proposal into skill edits and everything else. Skill edits are the
|
|
389
427
|
* governed part: they need explicit user consent before the gate applies
|
package/lib/command.d.ts
CHANGED
|
@@ -18,9 +18,8 @@ export interface CommandRuntimeOptions {
|
|
|
18
18
|
/** Mechanical promotion guards for wrapup/fate (2026-08-22 policy). */
|
|
19
19
|
promotionPolicy: PromotionPolicy;
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* switch still works.
|
|
21
|
+
* Initial auto-review default from plugin config. The listener is always
|
|
22
|
+
* registered; this is not a registration gate.
|
|
24
23
|
*/
|
|
25
24
|
autoReview?: boolean;
|
|
26
25
|
}
|
package/lib/command.js
CHANGED
|
@@ -376,18 +376,22 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
|
|
|
376
376
|
}
|
|
377
377
|
case "pause":
|
|
378
378
|
case "resume": {
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
//
|
|
379
|
+
// The switch is independent of the static registration flag. A
|
|
380
|
+
// resume explicitly enables automatic snapshots; a pause leaves
|
|
381
|
+
// the enable bit intact so status explains the paused state.
|
|
382
382
|
const pausing = sub === "pause";
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
|
|
383
|
+
const defaultEnabled = runtime.autoReview ?? false;
|
|
384
|
+
const current = loadGateRuntime(engine.baseDir, defaultEnabled);
|
|
385
|
+
if (pausing && current.paused) {
|
|
386
|
+
return success("auto-review gate is already paused (no change).");
|
|
386
387
|
}
|
|
387
|
-
|
|
388
|
+
if (!pausing && current.enabled && !current.paused) {
|
|
389
|
+
return success("auto-review gate is already running (no change).");
|
|
390
|
+
}
|
|
391
|
+
saveGateRuntime(engine.baseDir, pausing, pausing ? current.enabled : true);
|
|
388
392
|
return success(pausing
|
|
389
|
-
? "auto-review gate paused: no automatic
|
|
390
|
-
: "auto-review gate resumed:
|
|
393
|
+
? "auto-review gate paused: no automatic snapshots, fate assessments, or gate LLM calls until /evolve resume. Manual evolve_* tools and /evolve commands keep working."
|
|
394
|
+
: "auto-review gate resumed: successful turns will produce incremental snapshots immediately.");
|
|
391
395
|
}
|
|
392
396
|
case "status": {
|
|
393
397
|
return success(renderGateStatus(engine, sessionId, projectKeyOf(invocation.agent), runtime));
|
|
@@ -454,16 +458,14 @@ function demoteEntry(engine, id, sessionId, projectKey) {
|
|
|
454
458
|
return error(`entry ${id} not found in the global, project, or local store`);
|
|
455
459
|
}
|
|
456
460
|
/**
|
|
457
|
-
*
|
|
458
|
-
*
|
|
459
|
-
* the runtime switch says whether the human paused it since.
|
|
461
|
+
* Status answers whether the always-registered listener is enabled at runtime
|
|
462
|
+
* and what stores/retention it currently sees.
|
|
460
463
|
*/
|
|
461
464
|
function renderGateStatus(engine, sessionId, projectKey, runtime) {
|
|
462
|
-
const
|
|
463
|
-
const
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
: `gate: ${patch === "unknown" ? "patch flag unknown" : "enabled in patch config"} · ${paused ? "PAUSED by /evolve pause (resume with /evolve resume)" : "running"}`;
|
|
465
|
+
const configuredDefault = runtime.autoReview === undefined ? "unknown" : runtime.autoReview ? "on" : "off";
|
|
466
|
+
const current = loadGateRuntime(engine.baseDir, runtime.autoReview ?? false);
|
|
467
|
+
const effective = current.paused ? "PAUSED (resume with /evolve resume)" : current.enabled ? "running" : "off (enable with /evolve resume)";
|
|
468
|
+
const gateLine = `gate: listener registered · config default ${configuredDefault} · runtime ${effective}`;
|
|
467
469
|
const countEntries = (state) => Object.values(state.entries).reduce((n, byKind) => n + Object.keys(byKind).length, 0);
|
|
468
470
|
const lines = [gateLine];
|
|
469
471
|
lines.push(`stores: global ${countEntries(engine.load("global", undefined))} entries · local(${sessionId}) ${countEntries(engine.load("local", sessionId))} entries`);
|
package/lib/fate.d.ts
CHANGED
|
@@ -119,7 +119,7 @@ export interface FateApplyResult {
|
|
|
119
119
|
export declare function applyLocalFates(engine: EvolutionEngine, sessionId: string, plan: FatePlan, localState: HarnessState, mode: FateApplyMode): FateApplyResult;
|
|
120
120
|
/**
|
|
121
121
|
* The gate's local-fate phase. Runs after the review phase on every gate
|
|
122
|
-
* trigger (
|
|
122
|
+
* trigger (turn_snapshot / compact), subject to cadence and cooldown. All
|
|
123
123
|
* failures are contained and recorded — a broken fate dimension never
|
|
124
124
|
* disturbs the agent loop.
|
|
125
125
|
*/
|
package/lib/fate.js
CHANGED
|
@@ -192,7 +192,7 @@ export function applyLocalFates(engine, sessionId, plan, localState, mode) {
|
|
|
192
192
|
}
|
|
193
193
|
/**
|
|
194
194
|
* The gate's local-fate phase. Runs after the review phase on every gate
|
|
195
|
-
* trigger (
|
|
195
|
+
* trigger (turn_snapshot / compact), subject to cadence and cooldown. All
|
|
196
196
|
* failures are contained and recorded — a broken fate dimension never
|
|
197
197
|
* disturbs the agent loop.
|
|
198
198
|
*/
|
|
@@ -255,7 +255,7 @@ export async function runLocalFatePhase(ctx, engine, agent, config, state, reaso
|
|
|
255
255
|
rationale: `fate: ${assessment.rationale} (${applied.join("; ")})`,
|
|
256
256
|
refinementId: results.map((result) => result.id).join(","),
|
|
257
257
|
});
|
|
258
|
-
if (config.notifyOnAutoReview && reason === "turn_interval" && applied.length > 0) {
|
|
258
|
+
if (config.notifyOnAutoReview && (reason === "turn_snapshot" || reason === "turn_interval") && applied.length > 0) {
|
|
259
259
|
notifyFateApplied(ctx, agent, applied);
|
|
260
260
|
}
|
|
261
261
|
return;
|
|
@@ -309,7 +309,7 @@ export async function runLocalFatePhase(ctx, engine, agent, config, state, reaso
|
|
|
309
309
|
rationale: `fate: ${assessment.rationale} (${applied.join("; ")})`,
|
|
310
310
|
refinementId: results.map((result) => result.id).join(","),
|
|
311
311
|
});
|
|
312
|
-
if (config.notifyOnAutoReview && reason === "turn_interval") {
|
|
312
|
+
if (config.notifyOnAutoReview && (reason === "turn_snapshot" || reason === "turn_interval")) {
|
|
313
313
|
notifyFateApplied(ctx, agent, applied);
|
|
314
314
|
}
|
|
315
315
|
return;
|
|
@@ -333,7 +333,7 @@ export function buildFateNotice(applied) {
|
|
|
333
333
|
"请用一句话简短确认即可,不要调用任何工具。",
|
|
334
334
|
].join("\n");
|
|
335
335
|
}
|
|
336
|
-
/** Queue the follow-up notice turn (
|
|
336
|
+
/** Queue the follow-up notice turn (successful-turn path only). */
|
|
337
337
|
function notifyFateApplied(ctx, agent, applied) {
|
|
338
338
|
try {
|
|
339
339
|
agent.followup(createUserMessage({
|
package/lib/index.d.ts
CHANGED
|
@@ -10,9 +10,9 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
10
10
|
baseDir: z<string, string, "plain">;
|
|
11
11
|
/** System-prompt section order for the evolution guidance. */
|
|
12
12
|
sectionOrder: z<number, number, "defined">;
|
|
13
|
-
/**
|
|
13
|
+
/** Initial automatic-review default when no runtime switch exists (off by default: it costs model calls). */
|
|
14
14
|
autoReview: z<boolean, boolean, "defined">;
|
|
15
|
-
/**
|
|
15
|
+
/** Legacy/local-fate cadence fallback; successful-turn review is snapshot-driven. */
|
|
16
16
|
reviewIntervalTurns: z<number, number, "defined">;
|
|
17
17
|
/** Trajectory slice handed to the gate, in characters. */
|
|
18
18
|
maxReviewInputChars: z<number, number, "defined">;
|
|
@@ -107,9 +107,9 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
107
107
|
baseDir: z<string, string, "plain">;
|
|
108
108
|
/** System-prompt section order for the evolution guidance. */
|
|
109
109
|
sectionOrder: z<number, number, "defined">;
|
|
110
|
-
/**
|
|
110
|
+
/** Initial automatic-review default when no runtime switch exists (off by default: it costs model calls). */
|
|
111
111
|
autoReview: z<boolean, boolean, "defined">;
|
|
112
|
-
/**
|
|
112
|
+
/** Legacy/local-fate cadence fallback; successful-turn review is snapshot-driven. */
|
|
113
113
|
reviewIntervalTurns: z<number, number, "defined">;
|
|
114
114
|
/** Trajectory slice handed to the gate, in characters. */
|
|
115
115
|
maxReviewInputChars: z<number, number, "defined">;
|