dsh-continual-evolve 0.7.4 → 0.7.5

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 CHANGED
@@ -19,14 +19,14 @@ Agents accumulate reusable experience (repeated failures, durable facts, reusabl
19
19
 
20
20
  - **Three scopes** with merge semantics (global < project < local): **local** per-session staging, **project** per-workspace cross-session store, **global** cross-project — plus mechanical promotion guards so only portable, substantial, non-duplicate knowledge reaches global
21
21
  - **Typed one-fact memories**: every memory entry carries a recall type (`user | feedback | project | reference`); pitfalls (`feedback`) must include Why + How to apply
22
- - **Dedicated background memory agent**: after eligible successful turns, a bounded ZCode-style loop searches the frozen memory manifest and proposes memory-only edits through a closed tool set; it cannot call agents, MCP, the network, or write source files
22
+ - **Dedicated background memory agent**: when `autoReview: true`, eligible successful turns feed a bounded ZCode-style loop that searches the frozen memory manifest and proposes memory-only edits through a closed tool set; it cannot call agents, MCP, the network, or write source files. The generic review/planner/fate path is not run by this listener
23
23
  - **Deterministic rollback**: inverse edits generated from applied results — no LLM re-guessing
24
24
  - **Benchmark loop**: candidate refinements are evaluated against frozen cases by a separate scorer before acceptance (rubric encrypted at rest)
25
25
  - **Store hygiene**: `/evolve consolidate` turns write-time conflict hints and zero-use staleness into one approved, fully reversible batch of archives — with `merge`, near-duplicate content folds into the surviving original
26
26
 
27
27
  ## How it works
28
28
 
29
- 1. **Sediment** — the model creates entries via `evolve_add`, or the automatic pipeline consumes incremental snapshots captured after successful turns (plus compaction checkpoints): a dedicated memory agent runs first, then the general review/planner proposes non-memory refinements.
29
+ 1. **Sediment** — the model creates entries via `evolve_add`, or (only when `autoReview: true`) the automatic Memory Agent consumes incremental snapshots after successful turns (plus compaction checkpoints). Generic review/planner remains manual unless separately invoked.
30
30
  2. **Capability-aware auxiliary calls** — the memory loop, review, planner, wrapup, and fate resolve exact provider/model metadata through [`src/llm-text.ts`](src/llm-text.ts), use the lowest advertised enabled reasoning effort (falling back to a closing effort only when no enabled level exists), and forward the host session id for provider routing; models without reasoning metadata use their provider default.
31
31
  3. **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.
32
32
  4. **Approve** — global and project writes require explicit human approval; the dialog shows the bounded structured edit diff and conflict warnings, while malformed/lost responses remain retryable rather than counting as rejection.
@@ -79,13 +79,14 @@ Injection shape: prompt notes and delegation specs inject with content (≤6/kin
79
79
  | Key | Default | Meaning |
80
80
  |---|---|---|
81
81
  | `baseDir` | resolved DSH home | root for the `evolve/` stores |
82
- | `autoReview` | `false` | initial memory+review pipeline default when no runtime switch exists; the listener is always registered |
82
+ | `autoReview` | `false` | explicit opt-in for the dedicated Memory Agent listener; when true, generic review/planner/fate are not run |
83
+ | `memoryMinUserWords` | `3` | ZCode-style minimum lexical words in one direct user text part; uses CJK-aware segmentation |
83
84
  | `reviewIntervalTurns` | `6` | legacy local-fate cadence fallback; successful-turn review no longer waits for this interval |
84
85
  | `maxReviewInputChars` | `40000` | trajectory slice handed to the gate |
85
86
  | `reviewBudgetTokens` | `4096` | output budget for the gate call |
86
87
  | `notifyOnAutoReview` | `true` | visible follow-up notice after an applied gate run |
87
88
  | `requireGlobalApproval` | `true` | global and project edits ask for explicit approval |
88
- | `localFate` | `true` | gate audits local entries and proposes promote/archive (consulted, never silent) |
89
+ | `localFate` | `false` | optional local-entry promote/archive fate assessment; requires `autoReview: true` and an explicit `localFate: true` |
89
90
  | `fateIntervalTurns` | follows `reviewIntervalTurns` | minimum turns between fate assessments |
90
91
  | `goalBlockedWrapupTurns` | `3` | consecutive blocked-goal gate runs trigger one fate assessment (`0` disables) |
91
92
  | `promotionBlockPatterns` | POSIX paths, session ids, `~/.dsh` | content matching these is project-scoped and never promoted to global |
@@ -111,18 +112,23 @@ Example profile patch:
111
112
  reviewIntervalTurns: 6
112
113
  ```
113
114
 
114
- The listener is registered even when `autoReview` is `false`. Use `/evolve resume`
115
- to enable successful-turn snapshots immediately, `/evolve pause` to suppress new
116
- snapshots and fate/model work, and `/evolve status` to inspect the configured
117
- default plus the current runtime state. The runtime switch is stored in
118
- `evolve/runtime.json`; manual `evolve_*` tools and `/evolve` commands are not
119
- paused.
115
+ Automatic evolution is an explicit source-level opt-in. With the default
116
+ `autoReview: false`, the plugin does not register turn/compaction listeners.
117
+ With `autoReview: true`, it registers only the dedicated Memory Agent listener;
118
+ the generic review/planner, prompt/skill writes, and local-fate phases are not
119
+ run automatically. The memory trigger follows ZCode's lightweight eligibility:
120
+ direct user text must contain at least `memoryMinUserWords` lexical words
121
+ (CJK-aware segmentation), while empty/internal/direct-memory-write snapshots
122
+ are skipped; compaction does not add a separate memory-only trigger. Manual
123
+ `evolve_*` tools and `/evolve` commands remain available. `/evolve pause`,
124
+ `/evolve resume`, and `/evolve status` control or report the Memory Agent
125
+ listener.
120
126
 
121
127
  ## Development
122
128
 
123
129
  ```bash
124
130
  pnpm install && pnpm build # deps + tsc -> lib/
125
- pnpm test # vitest (737 tests)
131
+ pnpm test # vitest (743 tests)
126
132
  pnpm test:coverage # v8 coverage, thresholds enforced in CI
127
133
  pnpm lint # oxlint src test
128
134
  ```
@@ -131,7 +137,7 @@ Project layout:
131
137
 
132
138
  ```
133
139
  ├── src/ # engine, tools, commands, memory agent, gate, fate, benchmark, injection + token usage…
134
- ├── test/ # vitest suites (44 files)
140
+ ├── test/ # vitest suites (45 files)
135
141
  ├── lib/ # build output (tsc)
136
142
  ├── docs/
137
143
  │ ├── design.md # full design doc (hardening matrix)
package/README.zh.md CHANGED
@@ -19,14 +19,14 @@ Agent 在每个会话里积累可复用经验(重复失败、持久事实、
19
19
 
20
20
  - **三作用域**与合并语义(global < project < local):**local** 本会话暂存、**project** 本项目跨会话库、**global** 跨项目——配合机械化晋升守卫,只有可携带、有分量、非重复的知识才能进全局
21
21
  - **类型化单条记忆**:每条 memory 带召回类型(`user | feedback | project | reference`);踩坑(`feedback`)必须含 Why + How to apply
22
- - **专用后台记忆 Agent**:成功回合通过机械 eligibility 后,有界 ZCode 式 loop 检索冻结的 memory manifest,并经闭集工具提出纯 memory 编辑;不能调用 Agent、MCP、网络,也不能写源码
22
+ - **专用后台记忆 Agent**:`autoReview: true` 时,成功回合通过机械 eligibility 后才有界 ZCode 式 loop 检索冻结的 memory manifest,并经闭集工具提出纯 memory 编辑;不能调用 Agent、MCP、网络,也不能写源码。该 listener 不运行通用 review/planner/fate
23
23
  - **确定性回滚**:逆操作编辑由已应用结果生成——不靠 LLM 重新猜测
24
24
  - **benchmark 闭环**:候选沉淀先经冻结用例 + 独立评分者评估再接受(rubric 加密落盘)
25
25
  - **store 卫生**:`/evolve consolidate` 把写入时冲突提示与零使用陈旧条目变成一次批准、完全可逆的批量归档——加 `merge` 可将近重复内容并入幸存原条目
26
26
 
27
27
  ## 工作原理
28
28
 
29
- 1. **沉淀**——模型经 `evolve_add` 创建条目,或自动管线消费成功回合后的增量 snapshot(以及压缩检查点):先运行专用 memory Agent,再由通用 review/planner 提议非 memory 精炼。
29
+ 1. **沉淀**——模型经 `evolve_add` 创建条目,或仅在 `autoReview: true` 时由专用 Memory Agent 消费成功回合后的增量 snapshot(以及压缩检查点)。通用 review/planner 保持手动,除非另行显式调用。
30
30
  2. **能力感知的辅助调用**——memory loop、review、planner、wrapup、fate 通过 [`src/llm-text.ts`](src/llm-text.ts) 解析精确 provider/model 能力并使用模型公布的最低开启 reasoning effort,同时转发 host session id 供 provider 路由;只有没有开启档时才回退关闭档,没有 reasoning 元数据时使用 provider 默认行为。
31
31
  3. **守卫**——代码强制校验:编辑 schema、blast-radius 与作用域一致性、晋升政策(项目专属标记 / 过薄内容 / 近似重复检测 / 凭据筛查保持全局库干净——密钥类内容在所有写入出口被拒,含 mount 物化)。全局 create 与既有条目高度相似(≥0.8)时写入即拒;中等重叠带 `conflictHint` 供后续合并。
32
32
  4. **审批**——全局与项目写入需明确人工批准;弹窗展示有界结构化编辑 diff 与冲突提示,弹窗丢失/响应畸形会重试,不会被误记为拒绝。
@@ -79,13 +79,14 @@ dsh plugin add ZK-Andy/dsh-continual-evolve
79
79
  | 键 | 默认 | 含义 |
80
80
  |---|---|---|
81
81
  | `baseDir` | 解析后的 DSH home | `evolve/` 存储根目录 |
82
- | `autoReview` | `false` | 没有 runtime 开关时自动 memory+review 管线的初始默认;监听器始终注册 |
82
+ | `autoReview` | `false` | 专用 Memory Agent listener 的显式 opt-in;为 true 时也不会自动运行通用 review/planner/fate |
83
+ | `memoryMinUserWords` | `3` | ZCode 风格:单个直接用户文本至少包含的词数;使用 CJK 分词 |
83
84
  | `reviewIntervalTurns` | `6` | local-fate 的兼容节奏;成功回合 review 不再等待这个间隔 |
84
85
  | `maxReviewInputChars` | `40000` | 交给门禁的轨迹切片 |
85
86
  | `reviewBudgetTokens` | `4096` | 门禁调用输出预算 |
86
87
  | `notifyOnAutoReview` | `true` | 门禁应用后发可见跟进通知 |
87
88
  | `requireGlobalApproval` | `true` | 全局与项目写入需明确批准 |
88
- | `localFate` | `true` | 门禁审计本地条目并提议晋升/归档(先征询,绝不静默) |
89
+ | `localFate` | `false` | 可选的本地条目晋升/归档 fate 评估;需要同时设置 `autoReview: true` 与显式 `localFate: true` |
89
90
  | `fateIntervalTurns` | 跟随 `reviewIntervalTurns` | 归宿评估的最小回合间隔 |
90
91
  | `goalBlockedWrapupTurns` | `3` | 连续阻塞目标的门禁轮数触发一次归宿评估(`0` 关闭) |
91
92
  | `promotionBlockPatterns` | POSIX 路径、session id、`~/.dsh` | 内容命中即判定项目专属,永不晋升全局 |
@@ -111,13 +112,13 @@ profile patch 示例:
111
112
  reviewIntervalTurns: 6
112
113
  ```
113
114
 
114
- 即使 `autoReview` 为 `false`,监听器也会注册。使用 `/evolve resume` 立即开启成功回合 snapshot,使用 `/evolve pause` 抑制新 snapshot、fate 和模型调用,使用 `/evolve status` 查看配置默认值与运行时状态。开关保存在 `evolve/runtime.json`;手动 `evolve_*` 工具和 `/evolve` 命令不受暂停影响。
115
+ 自动进化是项目源码层面的显式 opt-in。默认 `autoReview: false` 时,插件不注册回合/压缩监听器。设置 `autoReview: true` 时只注册专用 Memory Agent listener,不自动运行通用 review/planner、prompt/skill 写入或 local fate。Memory 触发采用 ZCode 的轻量 eligibility:单个直接用户文本至少包含 `memoryMinUserWords` 个词(使用 CJK 分词);空增量、内部 Agent、直接 memory 写入会跳过,压缩不会额外触发 memory-only 管线。手动 `evolve_*` 工具和 `/evolve` 命令仍可用;`/evolve pause`、`/evolve resume`、`/evolve status` 控制或报告 Memory Agent listener。
115
116
 
116
117
  ## 开发
117
118
 
118
119
  ```bash
119
120
  pnpm install && pnpm build # 依赖 + tsc -> lib/
120
- pnpm test # vitest(737 例)
121
+ pnpm test # vitest(743 例)
121
122
  pnpm test:coverage # v8 覆盖率,CI 强制阈值
122
123
  pnpm lint # oxlint src test
123
124
  ```
@@ -126,7 +127,7 @@ pnpm lint # oxlint src test
126
127
 
127
128
  ```
128
129
  ├── src/ # 引擎、工具、命令、memory Agent、门禁、fate、benchmark、注入 + token 用量…
129
- ├── test/ # vitest 测试套件(44 个文件)
130
+ ├── test/ # vitest 测试套件(45 个文件)
130
131
  ├── lib/ # 构建产物(tsc)
131
132
  ├── docs/
132
133
  │ ├── design.md # 完整设计文档(硬化矩阵)
package/lib/auto.d.ts CHANGED
@@ -12,6 +12,13 @@ export interface AutoReviewConfig {
12
12
  intervalTurns: number;
13
13
  /** Initial runtime state when no runtime.json exists. */
14
14
  enabledByDefault?: boolean;
15
+ /** ZCode-style minimum lexical words in one direct user text part. */
16
+ memoryMinUserWords?: number;
17
+ /**
18
+ * Run only the dedicated memory extraction phase. The general review,
19
+ * planner, prompt/skill writes, and local-fate phases are never entered.
20
+ */
21
+ memoryOnly?: boolean;
15
22
  maxInputChars: number;
16
23
  budgetTokens: number;
17
24
  /** Queue a visible follow-up notice after an approved, applied gate run. */
package/lib/auto.js CHANGED
@@ -1,9 +1,10 @@
1
1
  /**
2
- * The automatic review driver: watches agent turns and session compaction,
3
- * runs the cheap review gate, and — when the gate approves — runs the
4
- * local-scope planner and applies the result. All auxiliary work is
5
- * fire-and-forget with error containment: an auto-review failure never
6
- * disturbs the agent loop.
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.
7
8
  *
8
9
  * Every gate decision (approved / declined / failed / skipped) is appended to
9
10
  * `<dshHome>/evolve/reviews.jsonl` so auto-review activity is durably
@@ -80,7 +81,12 @@ export function registerAutoReview(ctx, engine, config) {
80
81
  return "no-op";
81
82
  }
82
83
  try {
83
- await runGate(ctx, engine, agent, config, state, snapshot, record, signal);
84
+ if (config.memoryOnly) {
85
+ await runMemoryExtractionPhase(ctx, engine, agent, config, state, snapshot, signal);
86
+ }
87
+ else {
88
+ await runGate(ctx, engine, agent, config, state, snapshot, record, signal);
89
+ }
84
90
  return "success";
85
91
  }
86
92
  catch (cause) {
@@ -118,6 +124,7 @@ export function registerAutoReview(ctx, engine, config) {
118
124
  reason,
119
125
  ...(cursor !== undefined ? { cursor } : {}),
120
126
  maxChars: config.maxInputChars,
127
+ ...(config.memoryMinUserWords !== undefined ? { minUserWords: config.memoryMinUserWords } : {}),
121
128
  });
122
129
  if (!disposedSessions.has(agent.id))
123
130
  schedulerFor(agent, state).schedule(snapshot);
@@ -169,7 +176,7 @@ export function registerAutoReview(ctx, engine, config) {
169
176
  reason: "boot",
170
177
  turnsSinceLastReview: 0,
171
178
  outcome: "armed",
172
- rationale: `auto-review gate registered (default=${defaultEnabled ? "on" : "off"}; per-success-turn snapshots; local-fate every ${config.fateIntervalTurns} turns)`,
179
+ rationale: `automatic evolution listener registered (mode=${config.memoryOnly ? "memory-only" : "full"}; default=${defaultEnabled ? "on" : "off"}; per-success-turn snapshots)`,
173
180
  })}\n`, "utf8");
174
181
  }
175
182
  catch (cause) {
@@ -192,6 +199,11 @@ export function registerAutoReview(ctx, engine, config) {
192
199
  perSession.delete(agent.id);
193
200
  });
194
201
  ctx.on("session/event", (session, event) => {
202
+ // ZCode schedules extraction from successful turns, not from compaction.
203
+ // Keep the legacy compaction path for direct/full callers, but do not
204
+ // add an extra automatic Memory Agent trigger in memory-only mode.
205
+ if (config.memoryOnly)
206
+ return;
195
207
  if (event.type !== "compaction/start")
196
208
  return;
197
209
  const agents = ctx.agents;
package/lib/command.d.ts CHANGED
@@ -17,11 +17,10 @@ export interface CommandRuntimeOptions {
17
17
  autoCase: boolean;
18
18
  /** Mechanical promotion guards for wrapup/fate (2026-08-22 policy). */
19
19
  promotionPolicy: PromotionPolicy;
20
- /**
21
- * Initial auto-review default from plugin config. The listener is always
22
- * registered; this is not a registration gate.
23
- */
20
+ /** Explicit source-level opt-in for the automatic listener. `false` means no wiring. */
24
21
  autoReview?: boolean;
22
+ /** The automatic listener runs only the dedicated Memory Agent. */
23
+ memoryOnly?: boolean;
25
24
  }
26
25
  export declare function registerEvolveCommand(ctx: Context, engine: EvolutionEngine, opts: CommandGateOptions, runtime: CommandRuntimeOptions): void;
27
26
  /**
package/lib/command.js CHANGED
@@ -44,8 +44,8 @@ const USAGE = `Usage:
44
44
  /evolve goal show the evolution goal (round-driven auto-review)
45
45
  /evolve goal <objective> create/update the evolution goal
46
46
  /evolve goal done complete the evolution goal
47
- /evolve pause | resume pause/resume the auto-review gate (manual tools and commands keep working)
48
- /evolve status gate state (patch flag + runtime switch) plus store entry counts
47
+ /evolve pause | resume pause/resume the automatic Memory Agent (manual tools and commands keep working)
48
+ /evolve status automatic Memory Agent state plus store entry counts
49
49
  /evolve usage injection counts + exact direct-call token usage (benchmark subagents excluded)`;
50
50
  export function registerEvolveCommand(ctx, engine, opts, runtime) {
51
51
  ctx.commands.register({
@@ -376,22 +376,26 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
376
376
  }
377
377
  case "pause":
378
378
  case "resume": {
379
+ if (runtime.autoReview === false) {
380
+ return success("automatic evolution wiring is disabled by project policy; no automatic Memory Agent listener is registered. Manual evolve_* tools and /evolve commands keep working.");
381
+ }
382
+ const memoryOnly = runtime.memoryOnly !== false;
379
383
  // 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.
384
+ // resume explicitly enables the Memory Agent; a pause leaves the
385
+ // enable bit intact so status explains the paused state.
382
386
  const pausing = sub === "pause";
383
387
  const defaultEnabled = runtime.autoReview ?? false;
384
388
  const current = loadGateRuntime(engine.baseDir, defaultEnabled);
385
389
  if (pausing && current.paused) {
386
- return success("auto-review gate is already paused (no change).");
390
+ return success("automatic Memory Agent is already paused (no change).");
387
391
  }
388
392
  if (!pausing && current.enabled && !current.paused) {
389
- return success("auto-review gate is already running (no change).");
393
+ return success(memoryOnly ? "automatic Memory Agent is already running (no change)." : "automatic evolution is already running (no change).");
390
394
  }
391
395
  saveGateRuntime(engine.baseDir, pausing, pausing ? current.enabled : true);
392
396
  return success(pausing
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.");
397
+ ? "automatic Memory Agent paused: no automatic memory extraction calls until /evolve resume. Manual evolve_* tools and /evolve commands keep working."
398
+ : "automatic Memory Agent resumed: successful turns will produce incremental memory snapshots immediately.");
395
399
  }
396
400
  case "status": {
397
401
  return success(renderGateStatus(engine, sessionId, projectKeyOf(invocation.agent), runtime));
@@ -458,14 +462,22 @@ function demoteEntry(engine, id, sessionId, projectKey) {
458
462
  return error(`entry ${id} not found in the global, project, or local store`);
459
463
  }
460
464
  /**
461
- * Status answers whether the always-registered listener is enabled at runtime
462
- * and what stores/retention it currently sees.
465
+ * Status answers whether the automatic listener is wired and what
466
+ * stores/retention it currently sees.
463
467
  */
464
468
  function renderGateStatus(engine, sessionId, projectKey, runtime) {
465
469
  const configuredDefault = runtime.autoReview === undefined ? "unknown" : runtime.autoReview ? "on" : "off";
466
470
  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}`;
471
+ const memoryOnly = runtime.memoryOnly !== false;
472
+ const effective = runtime.autoReview === false
473
+ ? "disabled by project policy"
474
+ : current.paused
475
+ ? "PAUSED (resume with /evolve resume)"
476
+ : current.enabled
477
+ ? memoryOnly ? "Memory Agent running" : "running"
478
+ : memoryOnly ? "Memory Agent off (enable with /evolve resume)" : "off (enable with /evolve resume)";
479
+ const listener = runtime.autoReview === false ? "not wired" : memoryOnly ? "Memory Agent-only listener registered" : "listener registered";
480
+ const gateLine = `gate: ${listener} · config default ${configuredDefault} · runtime ${effective}`;
469
481
  const countEntries = (state) => Object.values(state.entries).reduce((n, byKind) => n + Object.keys(byKind).length, 0);
470
482
  const lines = [gateLine];
471
483
  lines.push(`stores: global ${countEntries(engine.load("global", undefined))} entries · local(${sessionId}) ${countEntries(engine.load("local", sessionId))} entries`);
package/lib/index.d.ts CHANGED
@@ -10,8 +10,10 @@ 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
- /** Initial automatic-review default when no runtime switch exists (off by default: it costs model calls). */
13
+ /** Explicit opt-in for the dedicated Memory Agent listener (off by default). */
14
14
  autoReview: z<boolean, boolean, "defined">;
15
+ /** ZCode-style minimum lexical words in one direct user text part. */
16
+ memoryMinUserWords: z<number, number, "defined">;
15
17
  /** Legacy/local-fate cadence fallback; successful-turn review is snapshot-driven. */
16
18
  reviewIntervalTurns: z<number, number, "defined">;
17
19
  /** Trajectory slice handed to the gate, in characters. */
@@ -58,7 +60,8 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
58
60
  /**
59
61
  * Gate local-fate dimension (#11 P2): the gate audits the session's local
60
62
  * entries on its own cadence and proposes promote/archive — consulted
61
- * first, never written silently. Only meaningful with autoReview on.
63
+ * first, never written silently. Only meaningful with autoReview on;
64
+ * disabled by default even when the automatic pipeline is explicitly enabled.
62
65
  */
63
66
  localFate: z<boolean, boolean, "defined">;
64
67
  /**
@@ -107,8 +110,10 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
107
110
  baseDir: z<string, string, "plain">;
108
111
  /** System-prompt section order for the evolution guidance. */
109
112
  sectionOrder: z<number, number, "defined">;
110
- /** Initial automatic-review default when no runtime switch exists (off by default: it costs model calls). */
113
+ /** Explicit opt-in for the dedicated Memory Agent listener (off by default). */
111
114
  autoReview: z<boolean, boolean, "defined">;
115
+ /** ZCode-style minimum lexical words in one direct user text part. */
116
+ memoryMinUserWords: z<number, number, "defined">;
112
117
  /** Legacy/local-fate cadence fallback; successful-turn review is snapshot-driven. */
113
118
  reviewIntervalTurns: z<number, number, "defined">;
114
119
  /** Trajectory slice handed to the gate, in characters. */
@@ -155,7 +160,8 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
155
160
  /**
156
161
  * Gate local-fate dimension (#11 P2): the gate audits the session's local
157
162
  * entries on its own cadence and proposes promote/archive — consulted
158
- * first, never written silently. Only meaningful with autoReview on.
163
+ * first, never written silently. Only meaningful with autoReview on;
164
+ * disabled by default even when the automatic pipeline is explicitly enabled.
159
165
  */
160
166
  localFate: z<boolean, boolean, "defined">;
161
167
  /**
@@ -209,5 +215,7 @@ export interface EvolutionService {
209
215
  readonly engine: EvolutionEngine;
210
216
  readonly baseDir: string;
211
217
  }
218
+ /** True only when the project explicitly opts into automatic evolution wiring. */
219
+ export declare function automaticEvolutionWired(config: Pick<EvolveConfig, "autoReview">): boolean;
212
220
  export declare function apply(ctx: Context, config: EvolveConfig): void;
213
221
  //# sourceMappingURL=index.d.ts.map
package/lib/index.js CHANGED
@@ -3,10 +3,10 @@
3
3
  *
4
4
  * Mounts the evolution engine, registers the model-facing evolve_* tools,
5
5
  * the human-facing /evolve command, the system-prompt guidance section, and
6
- * the automatic memory+review pipeline. The listener is always registered,
7
- * while its effective state is controlled by the runtime switch. Successful
8
- * turns feed a dedicated memory agent and general review through one
9
- * per-session serial scheduler.
6
+ * (only under an explicit `autoReview: true` opt-in) the dedicated Memory
7
+ * Agent listener. With the default `autoReview: false`, the automatic
8
+ * turn/compaction listeners are not registered; manual tools and commands
9
+ * remain available.
10
10
  */
11
11
  import { join } from "node:path";
12
12
  import z from "@deepseek-ai/schemastery";
@@ -30,8 +30,10 @@ export const Config = z.object({
30
30
  baseDir: z.string(),
31
31
  /** System-prompt section order for the evolution guidance. */
32
32
  sectionOrder: z.natural().default(118),
33
- /** Initial automatic-review default when no runtime switch exists (off by default: it costs model calls). */
33
+ /** Explicit opt-in for the dedicated Memory Agent listener (off by default). */
34
34
  autoReview: z.boolean().default(false),
35
+ /** ZCode-style minimum lexical words in one direct user text part. */
36
+ memoryMinUserWords: z.natural().default(3),
35
37
  /** Legacy/local-fate cadence fallback; successful-turn review is snapshot-driven. */
36
38
  reviewIntervalTurns: z.natural().default(6),
37
39
  /** Trajectory slice handed to the gate, in characters. */
@@ -78,9 +80,10 @@ export const Config = z.object({
78
80
  /**
79
81
  * Gate local-fate dimension (#11 P2): the gate audits the session's local
80
82
  * entries on its own cadence and proposes promote/archive — consulted
81
- * first, never written silently. Only meaningful with autoReview on.
83
+ * first, never written silently. Only meaningful with autoReview on;
84
+ * disabled by default even when the automatic pipeline is explicitly enabled.
82
85
  */
83
- localFate: z.boolean().default(true),
86
+ localFate: z.boolean().default(false),
84
87
  /**
85
88
  * Minimum turns between local-fate assessments on the turn-interval path
86
89
  * (compaction is unconditional). Absent → follows reviewIntervalTurns.
@@ -118,6 +121,10 @@ export const Config = z.object({
118
121
  tokenUsage: z.natural(),
119
122
  }),
120
123
  });
124
+ /** True only when the project explicitly opts into automatic evolution wiring. */
125
+ export function automaticEvolutionWired(config) {
126
+ return config.autoReview === true;
127
+ }
121
128
  export function apply(ctx, config) {
122
129
  const baseDir = resolveDshHome(config.baseDir);
123
130
  const skillsRoot = config.skillsDir ? expandHomePath(config.skillsDir) : join(baseDir, "skills");
@@ -163,7 +170,8 @@ export function apply(ctx, config) {
163
170
  autoRollbackOnReject: config.autoRollbackOnReject ?? true,
164
171
  autoCase: config.autoCase ?? true,
165
172
  promotionPolicy,
166
- ...(config.autoReview !== undefined ? { autoReview: config.autoReview } : {}),
173
+ autoReview: config.autoReview ?? false,
174
+ memoryOnly: config.autoReview === true,
167
175
  });
168
176
  // Plugin-owned file logging: every cordis log message lands in
169
177
  // <baseDir>/evolve/plugin.log regardless of how dsh web was launched —
@@ -178,29 +186,36 @@ export function apply(ctx, config) {
178
186
  void restoreMounted(ctx, baseDir).catch((cause) => {
179
187
  ctx.logger("continual-evolve").warn(`mount restore failed: ${cause instanceof Error ? cause.message : String(cause)}`);
180
188
  });
181
- // The listener is always registered. `autoReview` is only the initial
182
- // runtime default; `/evolve resume` can enable it without a profile edit
183
- // or process restart, while manual tools and commands stay independent.
184
- registerAutoReview(ctx, engine, {
185
- intervalTurns: config.reviewIntervalTurns ?? 6,
186
- enabledByDefault: config.autoReview ?? false,
187
- maxInputChars: config.maxReviewInputChars ?? 40000,
188
- budgetTokens: config.reviewBudgetTokens ?? 4096,
189
- notifyOnAutoReview: config.notifyOnAutoReview ?? true,
190
- localFate: config.localFate ?? true,
191
- fateIntervalTurns: config.fateIntervalTurns ?? config.reviewIntervalTurns ?? 6,
192
- goalBlockedWrapupTurns: config.goalBlockedWrapupTurns ?? 3,
193
- promotionPolicy,
194
- autoCase: config.autoCase ?? true,
195
- rubricKey,
196
- ...(config.historyRetain?.reviews !== undefined ? { reviewsRetain: config.historyRetain.reviews } : {}),
197
- ...(config.reviewModel ? { reviewModel: config.reviewModel } : {}),
198
- requireGlobalApproval: config.requireGlobalApproval ?? true,
199
- ...(config.plannerPrefixCache ? { prefixCacheMode: config.plannerPrefixCache } : {}),
200
- ...(config.plannerPrefixMaxChars !== undefined ? { prefixMaxChars: config.plannerPrefixMaxChars } : {}),
201
- });
202
- const runtimeDefault = config.autoReview ?? false;
203
- ctx.logger("continual-evolve").info(`continual-evolve auto-review registered (default ${runtimeDefault ? "on" : "off"}; successful-turn memory agent + general review; local-fate ${config.localFate ?? true ? "on" : "off"} every ${config.fateIntervalTurns ?? config.reviewIntervalTurns ?? 6} turns)`);
189
+ // `autoReview` is an explicit opt-in for the dedicated Memory Agent only.
190
+ // The general review/planner, prompt/skill writes, and local-fate phases
191
+ // are not reachable from this listener. Manual tools and commands remain
192
+ // available below.
193
+ if (automaticEvolutionWired(config)) {
194
+ registerAutoReview(ctx, engine, {
195
+ intervalTurns: config.reviewIntervalTurns ?? 6,
196
+ enabledByDefault: true,
197
+ memoryOnly: true,
198
+ memoryMinUserWords: config.memoryMinUserWords ?? 3,
199
+ maxInputChars: config.maxReviewInputChars ?? 40000,
200
+ budgetTokens: config.reviewBudgetTokens ?? 4096,
201
+ notifyOnAutoReview: config.notifyOnAutoReview ?? true,
202
+ localFate: config.localFate ?? false,
203
+ fateIntervalTurns: config.fateIntervalTurns ?? config.reviewIntervalTurns ?? 6,
204
+ goalBlockedWrapupTurns: config.goalBlockedWrapupTurns ?? 3,
205
+ promotionPolicy,
206
+ autoCase: config.autoCase ?? true,
207
+ rubricKey,
208
+ ...(config.historyRetain?.reviews !== undefined ? { reviewsRetain: config.historyRetain.reviews } : {}),
209
+ ...(config.reviewModel ? { reviewModel: config.reviewModel } : {}),
210
+ requireGlobalApproval: config.requireGlobalApproval ?? true,
211
+ ...(config.plannerPrefixCache ? { prefixCacheMode: config.plannerPrefixCache } : {}),
212
+ ...(config.plannerPrefixMaxChars !== undefined ? { prefixMaxChars: config.plannerPrefixMaxChars } : {}),
213
+ });
214
+ ctx.logger("continual-evolve").info("continual-evolve automatic listener registered (Memory Agent only; generic review/planner/fate disconnected)");
215
+ }
216
+ else {
217
+ ctx.logger("continual-evolve").info("continual-evolve automatic evolution wiring disabled (autoReview is not explicitly true; manual tools and commands remain available)");
218
+ }
204
219
  ctx.logger("continual-evolve").info(`continual-evolve mounted (baseDir=${baseDir})`);
205
220
  }
206
221
  //# sourceMappingURL=index.js.map
@@ -24,6 +24,7 @@ export interface TurnSnapshot {
24
24
  readonly userText: string;
25
25
  readonly sourceSeqs: readonly number[];
26
26
  readonly maxChars: number;
27
+ readonly minUserWords: number;
27
28
  /** Cursor supplied by the shared scheduler before this snapshot selected events. */
28
29
  readonly capturedAfterCursor?: string;
29
30
  readonly eligible: boolean;
@@ -35,6 +36,8 @@ export interface SnapshotEligibility {
35
36
  readonly reason?: SnapshotSkipReason;
36
37
  readonly userText: string;
37
38
  }
39
+ /** ZCode's minimum user-prose threshold, counted as lexical words. */
40
+ export declare const MEMORY_MIN_USER_WORDS = 3;
38
41
  /**
39
42
  * Capture the current durable surface and select only rows after `cursor`.
40
43
  * A seq-bearing session uses `seq:<n>`; older/test surfaces fall back to an
@@ -45,6 +48,7 @@ export declare function captureTurnSnapshot(ctx: Context, agent: Agent, opts: {
45
48
  reason: TurnSnapshotReason;
46
49
  cursor?: string;
47
50
  maxChars: number;
51
+ minUserWords?: number;
48
52
  }): Promise<TurnSnapshot>;
49
53
  /**
50
54
  * Rebase one captured snapshot onto a phase-specific cursor while preserving
@@ -58,7 +62,7 @@ export declare function sliceTurnSnapshot(snapshot: TurnSnapshot, cursor: string
58
62
  * does not decide whether a fact is valuable; that remains the review model's
59
63
  * job. It only removes mechanical noise before the scheduler spends tokens.
60
64
  */
61
- export declare function evaluateSnapshotEligibility(events: readonly unknown[], internalAgent?: boolean): SnapshotEligibility;
65
+ export declare function evaluateSnapshotEligibility(events: readonly unknown[], internalAgent?: boolean, minUserWords?: number): SnapshotEligibility;
62
66
  /** True when an assistant row records an explicit evolve memory mutation. */
63
67
  export declare function containsDirectMemoryWrite(events: readonly unknown[]): boolean;
64
68
  /** Serialize only user/assistant text, retaining the bounded tail. */
@@ -1,5 +1,19 @@
1
1
  import { sessionEventsOf } from "./inject.js";
2
2
  import { projectKeyOf } from "./project.js";
3
+ /** ZCode's minimum user-prose threshold, counted as lexical words. */
4
+ export const MEMORY_MIN_USER_WORDS = 3;
5
+ const WORD_SEGMENTER = new Intl.Segmenter("und", { granularity: "word" });
6
+ function countUserWords(text) {
7
+ return [...WORD_SEGMENTER.segment(text)].filter((segment) => segment.isWordLike).length;
8
+ }
9
+ function containsEligibleUserProse(events, minUserWords) {
10
+ return events.some((event) => {
11
+ if (!isDirectUserEvent(event))
12
+ return false;
13
+ const row = event;
14
+ return countUserWords(contentText(row.data?.content)) >= minUserWords;
15
+ });
16
+ }
3
17
  /**
4
18
  * Capture the current durable surface and select only rows after `cursor`.
5
19
  * A seq-bearing session uses `seq:<n>`; older/test surfaces fall back to an
@@ -9,7 +23,8 @@ export async function captureTurnSnapshot(ctx, agent, opts) {
9
23
  const allEvents = await readSurface(ctx, agent);
10
24
  const cursor = boundaryCursor(allEvents);
11
25
  const events = eventsAfterCursor(allEvents, opts.cursor);
12
- const eligibility = evaluateSnapshotEligibility(events, isInternalAgent(agent));
26
+ const minUserWords = opts.minUserWords ?? MEMORY_MIN_USER_WORDS;
27
+ const eligibility = evaluateSnapshotEligibility(events, isInternalAgent(agent), minUserWords);
13
28
  const projectKey = projectKeyOf(agent);
14
29
  return {
15
30
  sessionId: agent.id,
@@ -22,6 +37,7 @@ export async function captureTurnSnapshot(ctx, agent, opts) {
22
37
  userText: eligibility.userText,
23
38
  sourceSeqs: directUserSeqs(events),
24
39
  maxChars: opts.maxChars,
40
+ minUserWords,
25
41
  ...(opts.cursor === undefined ? {} : { capturedAfterCursor: opts.cursor }),
26
42
  eligible: eligibility.eligible,
27
43
  ...(eligibility.reason ? { skipReason: eligibility.reason } : {}),
@@ -35,7 +51,7 @@ export async function captureTurnSnapshot(ctx, agent, opts) {
35
51
  */
36
52
  export function sliceTurnSnapshot(snapshot, cursor) {
37
53
  const events = sliceSnapshotEvents(snapshot, cursor);
38
- const eligibility = evaluateSnapshotEligibility(events, false);
54
+ const eligibility = evaluateSnapshotEligibility(events, false, snapshot.minUserWords);
39
55
  return {
40
56
  ...snapshot,
41
57
  events,
@@ -51,7 +67,7 @@ export function sliceTurnSnapshot(snapshot, cursor) {
51
67
  * does not decide whether a fact is valuable; that remains the review model's
52
68
  * job. It only removes mechanical noise before the scheduler spends tokens.
53
69
  */
54
- export function evaluateSnapshotEligibility(events, internalAgent = false) {
70
+ export function evaluateSnapshotEligibility(events, internalAgent = false, minUserWords = MEMORY_MIN_USER_WORDS) {
55
71
  if (internalAgent)
56
72
  return { eligible: false, reason: "internal-agent", userText: "" };
57
73
  if (events.length === 0)
@@ -59,7 +75,7 @@ export function evaluateSnapshotEligibility(events, internalAgent = false) {
59
75
  if (containsDirectMemoryWrite(events))
60
76
  return { eligible: false, reason: "direct-memory-write", userText: "" };
61
77
  const userText = directUserText(events);
62
- if (userText.length < 3)
78
+ if (!containsEligibleUserProse(events, minUserWords))
63
79
  return { eligible: false, reason: "no-user-prose", userText };
64
80
  return { eligible: true, userText };
65
81
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-continual-evolve",
3
- "version": "0.7.4",
3
+ "version": "0.7.5",
4
4
  "description": "Continual self-evolution plugin for DeepSeek Harness: versioned, auditable, rollback-safe harness state (prompt notes, memories, skills, subagent specs) refined from session trajectories.",
5
5
  "license": "MIT",
6
6
  "repository": {