dsh-continual-evolve 0.7.2 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  [![CI](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml/badge.svg)](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml)
8
8
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
9
9
  [![Node](https://img.shields.io/badge/node-%5E22.19%20%7C%7C%20%3E%3D24-339933)](package.json)
10
- [![Tests](https://img.shields.io/badge/tests-675%20passing-brightgreen)]()
10
+ [![Tests](https://img.shields.io/badge/tests-737%20passing-brightgreen)]()
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
 
@@ -19,17 +19,19 @@ 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
23
  - **Deterministic rollback**: inverse edits generated from applied results — no LLM re-guessing
23
24
  - **Benchmark loop**: candidate refinements are evaluated against frozen cases by a separate scorer before acceptance (rubric encrypted at rest)
24
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
25
26
 
26
27
  ## How it works
27
28
 
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
- 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
- 3. **Approve** — global and project writes require explicit human approval; local-fate proposals are consulted before they land.
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).
32
- 5. **Validate & roll back** — benchmarks score candidates against frozen cases; rejected candidates roll back deterministically and are captured as draft regression cases (`auto_regression` benchmark).
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.
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) and use the lowest advertised enabled reasoning effort (falling back to a closing effort only when no enabled level exists); models without reasoning metadata use their provider default.
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
+ 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.
33
+ 5. **Apply & inject** — memory batches preflight every persistent approval, recheck abort before writes, and compensate earlier scope writes if a later batch fails; every successful scope still passes through snapshot + audit. 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).
34
+ 6. **Validate & roll back** — benchmarks score candidates against frozen cases; rejected candidates roll back deterministically and are captured as draft regression cases (`auto_regression` benchmark).
33
35
 
34
36
  ## Install
35
37
 
@@ -62,13 +64,13 @@ Commands (in-session):
62
64
  | `/evolve goal [objective · done · block]` | round-driven auto-review goal |
63
65
  | `/evolve benchmark …` | case lifecycle, runs, acceptance |
64
66
  | `/evolve pause · resume · status` | pause/resume the auto-review gate (manual tools keep working), gate state |
65
- | `/evolve usage` | per-entry injection counts + exact provider-reported tokens for direct review/planner/wrapup/fate calls (benchmark host subagents excluded) |
67
+ | `/evolve usage` | per-entry injection counts + exact provider-reported tokens for direct memory/review/planner/wrapup/fate calls (benchmark host subagents excluded) |
66
68
 
67
69
  Model tools: `evolve_list / add / update / delete / rollback` (`evolve_delete` takes `id` or a batch `ids` array — one refinement, one approval).
68
70
 
69
71
  For third-party consumers: every applied evolution (gate or manual) appends a structured `evolve_complete` event to `reviews.jsonl` (`src/evolve-event.ts` defines the shape) alongside the human-readable audit records.
70
72
 
71
- `/evolve usage` also reads `evolve/token-usage.jsonl`: exact provider-reported input/cache/output/total tokens for the plugin's direct review, planner, manual-wrapup, and automatic-fate calls. The report covers a retained tail rather than lifetime usage, distinguishes missing provider samples, and explicitly excludes host benchmark subagents, their agent-loop calls, and per-entry injection attribution.
73
+ `/evolve usage` also reads `evolve/token-usage.jsonl`: exact provider-reported input/cache/output/total tokens for the plugin's direct memory-agent, review, planner, manual-wrapup, and automatic-fate calls. The report covers a retained tail rather than lifetime usage, distinguishes missing provider samples, and explicitly excludes host benchmark subagents, their agent-loop calls, and per-entry injection attribution.
72
74
 
73
75
  Injection shape: prompt notes and delegation specs inject with content (≤6/kind × 180 chars, relevance-ranked). Memories and skills appear as a relevance-ordered directory index (`[memory:type:id] title` hooks, capped at 15 lines with a fold counter) — full text via `evolve_list`. Empty store = zero injected tokens.
74
76
 
@@ -77,7 +79,7 @@ Injection shape: prompt notes and delegation specs inject with content (≤6/kin
77
79
  | Key | Default | Meaning |
78
80
  |---|---|---|
79
81
  | `baseDir` | resolved DSH home | root for the `evolve/` stores |
80
- | `autoReview` | `false` | initial automatic-review default when no runtime switch exists; the listener is always registered |
82
+ | `autoReview` | `false` | initial memory+review pipeline default when no runtime switch exists; the listener is always registered |
81
83
  | `reviewIntervalTurns` | `6` | legacy local-fate cadence fallback; successful-turn review no longer waits for this interval |
82
84
  | `maxReviewInputChars` | `40000` | trajectory slice handed to the gate |
83
85
  | `reviewBudgetTokens` | `4096` | output budget for the gate call |
@@ -95,7 +97,7 @@ Injection shape: prompt notes and delegation specs inject with content (≤6/kin
95
97
  | `logToFile` / `logLevel` / `logMaxBytes` | `true` / `1` / 5 MiB | plugin-owned JSONL file log with rotation |
96
98
  | `autoRollbackOnReject` | `true` | deterministic rollback after a benchmark rejection |
97
99
  | `autoCase` | `true` | failed evolution attempts are captured as draft regression cases (`auto_regression` benchmark) |
98
- | `reviewModel` | agent's own | optional cheaper model for the gate (`"provider/model"`) |
100
+ | `reviewModel` | agent's own | optional cheaper model for the dedicated memory agent and review gate (`"provider/model"`) |
99
101
  | `plannerPrefixCache` | `auto` | Route A session-prefix input when cache evidence exists (`session` always, `off` legacy flat text) |
100
102
  | `plannerPrefixMaxChars` | `12000` | session-prefix budget for Route A planning inputs (chars) |
101
103
  | `historyRetain` | `{snapshots: 20, refinements: 500, reviews: 500, tokenUsage: 500}` | storage hygiene: snapshots per store, tail lines per store history, shared `reviews.jsonl` tail, and direct-call `token-usage.jsonl` tail |
@@ -120,7 +122,7 @@ paused.
120
122
 
121
123
  ```bash
122
124
  pnpm install && pnpm build # deps + tsc -> lib/
123
- pnpm test # vitest (675 tests)
125
+ pnpm test # vitest (737 tests)
124
126
  pnpm test:coverage # v8 coverage, thresholds enforced in CI
125
127
  pnpm lint # oxlint src test
126
128
  ```
@@ -128,8 +130,8 @@ pnpm lint # oxlint src test
128
130
  Project layout:
129
131
 
130
132
  ```
131
- ├── src/ # engine, tools, commands, gate, fate, benchmark, injection + token usage…
132
- ├── test/ # vitest suites (43 files)
133
+ ├── src/ # engine, tools, commands, memory agent, gate, fate, benchmark, injection + token usage…
134
+ ├── test/ # vitest suites (44 files)
133
135
  ├── lib/ # build output (tsc)
134
136
  ├── docs/
135
137
  │ ├── design.md # full design doc (hardening matrix)
package/README.zh.md CHANGED
@@ -7,7 +7,7 @@
7
7
  [![CI](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml/badge.svg)](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml)
8
8
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
9
9
  [![Node](https://img.shields.io/badge/node-%5E22.19%20%7C%7C%20%3E%3D24-339933)](package.json)
10
- [![Tests](https://img.shields.io/badge/tests-675%20passing-brightgreen)]()
10
+ [![Tests](https://img.shields.io/badge/tests-737%20passing-brightgreen)]()
11
11
 
12
12
  [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(`dsh`)的持续自进化插件:一套**版本化、可审计、可回滚**的 harness 状态层——提示词补充、记忆、技能、子代理规格——从会话轨迹中沉淀而来。
13
13
 
@@ -19,17 +19,19 @@ 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
23
  - **确定性回滚**:逆操作编辑由已应用结果生成——不靠 LLM 重新猜测
23
24
  - **benchmark 闭环**:候选沉淀先经冻结用例 + 独立评分者评估再接受(rubric 加密落盘)
24
25
  - **store 卫生**:`/evolve consolidate` 把写入时冲突提示与零使用陈旧条目变成一次批准、完全可逆的批量归档——加 `merge` 可将近重复内容并入幸存原条目
25
26
 
26
27
  ## 工作原理
27
28
 
28
- 1. **沉淀**——模型经 `evolve_add` 创建条目,或自动 review 门禁从成功回合后的增量 snapshot(以及压缩检查点)提议。
29
- 2. **守卫**——代码强制校验:编辑 schema、blast-radius 与作用域一致性、晋升政策(项目专属标记 / 过薄内容 / 近似重复检测 / 凭据筛查保持全局库干净——密钥类内容在所有写入出口被拒,含 mount 物化)。全局 create 与既有条目高度相似(≥0.8)时写入即拒;中等重叠带 `conflictHint` 供后续合并。
30
- 3. **审批**——全局与项目写入需明确人工批准;local 归宿提议先征询后落地。
31
- 4. **应用与注入**——原子应用带快照与审计事件。prompt 补充与委派规格注入系统提示词(封顶、按相关性排序、被证伪条目降权、空 store 零 token);memory/skill 以按相关性排序的目录索引出现(`[memory:type:id] 标题`钩子)。
32
- 5. **验证与回滚**——benchmark 用冻结用例为候选打分;被拒候选确定性回滚,并自动沉淀为 draft 回归用例(`auto_regression` 基准)。
29
+ 1. **沉淀**——模型经 `evolve_add` 创建条目,或自动管线消费成功回合后的增量 snapshot(以及压缩检查点):先运行专用 memory Agent,再由通用 review/planner 提议非 memory 精炼。
30
+ 2. **能力感知的辅助调用**——memory loop、review、planner、wrapup、fate 通过 [`src/llm-text.ts`](src/llm-text.ts) 解析精确 provider/model 能力并使用模型公布的最低开启 reasoning effort;只有没有开启档时才回退关闭档,没有 reasoning 元数据时使用 provider 默认行为。
31
+ 3. **守卫**——代码强制校验:编辑 schema、blast-radius 与作用域一致性、晋升政策(项目专属标记 / 过薄内容 / 近似重复检测 / 凭据筛查保持全局库干净——密钥类内容在所有写入出口被拒,含 mount 物化)。全局 create 与既有条目高度相似(≥0.8)时写入即拒;中等重叠带 `conflictHint` 供后续合并。
32
+ 4. **审批**——全局与项目写入需明确人工批准;弹窗展示有界结构化编辑 diff 与冲突提示,弹窗丢失/响应畸形会重试,不会被误记为拒绝。
33
+ 5. **应用与注入**——memory 批次先完成所有持久化审批,写前重查 abort;后续 scope 失败时补偿回滚先前写入,成功 scope 仍保留快照与审计。prompt 补充与委派规格注入系统提示词(封顶、按相关性排序、被证伪条目降权、空 store 零 token);memory/skill 以按相关性排序的目录索引出现(`[memory:type:id] 标题`钩子)。
34
+ 6. **验证与回滚**——benchmark 用冻结用例为候选打分;被拒候选确定性回滚,并自动沉淀为 draft 回归用例(`auto_regression` 基准)。
33
35
 
34
36
  ## 安装
35
37
 
@@ -62,13 +64,13 @@ dsh plugin add ZK-Andy/dsh-continual-evolve
62
64
  | `/evolve goal [objective · done · block]` | 回合驱动的自进化目标 |
63
65
  | `/evolve benchmark …` | 用例生命周期、运行、接受决策 |
64
66
  | `/evolve pause · resume · status` | 暂停/恢复自动门禁(手动工具不受影响)、门禁状态 |
65
- | `/evolve usage` | 每条目注入次数 + review/planner/wrapup/fate 直属调用的 provider 精确 token(不含 benchmark 宿主子代理) |
67
+ | `/evolve usage` | 每条目注入次数 + memory/review/planner/wrapup/fate 直属调用的 provider 精确 token(不含 benchmark 宿主子代理) |
66
68
 
67
69
  模型工具:`evolve_list / add / update / delete / rollback`(`evolve_delete` 支持 `id` 或批量 `ids` 数组——一次 refinement、一次审批)。
68
70
 
69
71
  第三方消费:每次进化落地(门禁或手动)都会向 `reviews.jsonl` 追加结构化 `evolve_complete` 事件(shape 见 `src/evolve-event.ts`),与人类可读的审计记录并存。
70
72
 
71
- `/evolve usage` 还会读取 `evolve/token-usage.jsonl`:插件直属 review、planner、手动 wrapup 与自动 fate 调用的 provider 精确 input/cache/output/total token。报告只覆盖保留尾部而非终身累计,单独显示 provider 未返回 usage 的调用,并明确排除宿主 benchmark 子代理、其 agent-loop 调用与逐条 memory 注入归因。
73
+ `/evolve usage` 还会读取 `evolve/token-usage.jsonl`:插件直属 memory Agent、review、planner、手动 wrapup 与自动 fate 调用的 provider 精确 input/cache/output/total token。报告只覆盖保留尾部而非终身累计,单独显示 provider 未返回 usage 的调用,并明确排除宿主 benchmark 子代理、其 agent-loop 调用与逐条 memory 注入归因。
72
74
 
73
75
  注入形态:prompt 补充与委派规格带内容注入(每 kind ≤6 条 × 180 字符,按相关性排序)。memory/skill 以按相关性排序的目录索引出现(`[memory:type:id] 标题`钩子,15 行封顶 + 折叠计数行)——全文经 `evolve_list` 获取。空 store = 零注入 token。
74
76
 
@@ -77,7 +79,7 @@ dsh plugin add ZK-Andy/dsh-continual-evolve
77
79
  | 键 | 默认 | 含义 |
78
80
  |---|---|---|
79
81
  | `baseDir` | 解析后的 DSH home | `evolve/` 存储根目录 |
80
- | `autoReview` | `false` | 没有 runtime 开关时的自动 review 初始默认;监听器始终注册 |
82
+ | `autoReview` | `false` | 没有 runtime 开关时自动 memory+review 管线的初始默认;监听器始终注册 |
81
83
  | `reviewIntervalTurns` | `6` | local-fate 的兼容节奏;成功回合 review 不再等待这个间隔 |
82
84
  | `maxReviewInputChars` | `40000` | 交给门禁的轨迹切片 |
83
85
  | `reviewBudgetTokens` | `4096` | 门禁调用输出预算 |
@@ -95,7 +97,7 @@ dsh plugin add ZK-Andy/dsh-continual-evolve
95
97
  | `logToFile` / `logLevel` / `logMaxBytes` | `true` / `1` / 5 MiB | 插件自带 JSONL 文件日志带轮转 |
96
98
  | `autoRollbackOnReject` | `true` | benchmark 拒绝后自动确定性回滚 |
97
99
  | `autoCase` | `true` | 失败的进化尝试自动沉淀为 draft 回归用例(`auto_regression` 基准) |
98
- | `reviewModel` | agent 自身 | 门禁可选更便宜的模型(`"provider/model"`) |
100
+ | `reviewModel` | agent 自身 | 专用 memory Agent 与 review 门禁可选更便宜的模型(`"provider/model"`) |
99
101
  | `plannerPrefixCache` | `auto` | 有缓存证据时用会话前缀输入(`session` 总是前缀,`off` 保持旧扁平文本) |
100
102
  | `plannerPrefixMaxChars` | `12000` | Route A 会话前缀预算(字符) |
101
103
  | `historyRetain` | `{snapshots: 20, refinements: 500, reviews: 500, tokenUsage: 500}` | 存储卫生:每 store 快照数、每 store 历史尾行、共享 `reviews.jsonl` 尾行、直属调用 `token-usage.jsonl` 尾行 |
@@ -115,7 +117,7 @@ profile patch 示例:
115
117
 
116
118
  ```bash
117
119
  pnpm install && pnpm build # 依赖 + tsc -> lib/
118
- pnpm test # vitest(675 例)
120
+ pnpm test # vitest(737 例)
119
121
  pnpm test:coverage # v8 覆盖率,CI 强制阈值
120
122
  pnpm lint # oxlint src test
121
123
  ```
@@ -123,8 +125,8 @@ pnpm lint # oxlint src test
123
125
  目录结构:
124
126
 
125
127
  ```
126
- ├── src/ # 引擎、工具、命令、门禁、fate、benchmark、注入 + token 用量…
127
- ├── test/ # vitest 测试套件(43 个文件)
128
+ ├── src/ # 引擎、工具、命令、memory Agent、门禁、fate、benchmark、注入 + token 用量…
129
+ ├── test/ # vitest 测试套件(44 个文件)
128
130
  ├── lib/ # 构建产物(tsc)
129
131
  ├── docs/
130
132
  │ ├── design.md # 完整设计文档(硬化矩阵)
package/lib/approval.d.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  /**
2
- * Human approval gate for cross-session (global) evolution edits. Forward
3
- * edits to the shared global store require an explicit human "批准" before
4
- * they are applied; rollbacks (which restore prior recorded state) do not.
5
- * The engine itself stays a pure library — this is a policy at the boundary.
2
+ * Human approval gate for persistent-scope evolution edits. Project and
3
+ * global edits require an explicit human "批准" before they are applied;
4
+ * rollbacks (which restore prior recorded state) do not. The engine itself
5
+ * stays a pure library — this is a policy at the boundary.
6
6
  */
7
7
  import type { Context } from "@deepseek-ai/cordis";
8
8
  import type { Agent } from "@deepseek-ai/dsh-agent";
9
+ export type ScopeApprovalDecision = "approved" | "declined";
9
10
  export interface QuestionService {
10
11
  ask(request: {
11
12
  questions: {
@@ -31,6 +32,12 @@ export interface QuestionService {
31
32
  * whether that is an error or a fallback.
32
33
  */
33
34
  export declare function questionServiceOf(ctx: Context): QuestionService | undefined;
35
+ /**
36
+ * Ask once for a persistent-scope edit. Only a unique, explicit approval
37
+ * choice is accepted. A missing/ambiguous/unknown answer throws so callers
38
+ * never interpret a lost or malformed dialog as a durable rejection.
39
+ */
40
+ export declare function requestScopeApproval(ctx: Context, agent: Agent | undefined, signal: AbortSignal | undefined, scope: "project" | "global", what: string): Promise<ScopeApprovalDecision>;
34
41
  /**
35
42
  * Ask the user to approve a global edit. Throws when the service is missing,
36
43
  * the user declines, or the question cannot be answered.
package/lib/approval.js CHANGED
@@ -7,28 +7,44 @@ export function questionServiceOf(ctx) {
7
7
  return ctx.userQuestions;
8
8
  }
9
9
  /**
10
- * Ask the user to approve a global edit. Throws when the service is missing,
11
- * the user declines, or the question cannot be answered.
10
+ * Ask once for a persistent-scope edit. Only a unique, explicit approval
11
+ * choice is accepted. A missing/ambiguous/unknown answer throws so callers
12
+ * never interpret a lost or malformed dialog as a durable rejection.
12
13
  */
13
- export async function requireGlobalApproval(ctx, agent, signal, what) {
14
+ export async function requestScopeApproval(ctx, agent, signal, scope, what) {
14
15
  const userQuestions = questionServiceOf(ctx);
15
16
  if (!userQuestions) {
16
17
  throw new Error("global evolution edits require the userQuestions service (load @deepseek-ai/dsh-user-questions)");
17
18
  }
19
+ const storeLabel = scope === "project" ? "本项目跨会话 store" : "跨会话全局 store";
18
20
  const answer = await userQuestions.ask({
19
21
  questions: [
20
22
  {
21
23
  id: "approve-global-evolve",
22
- question: `批准写入跨会话全局 store?\n\n${what}`,
24
+ question: `批准写入${storeLabel}?\n\n${what}`,
23
25
  options: [{ label: "批准" }, { label: "拒绝" }],
24
26
  },
25
27
  ],
26
28
  ...(agent ? { agent } : {}),
27
29
  ...(signal ? { signal } : {}),
28
30
  });
29
- const item = answer.answers?.find((entry) => entry.id === "approve-global-evolve");
30
- const approved = item?.selected?.includes("批准") ?? false;
31
- if (!approved) {
31
+ const matching = answer.answers?.filter((entry) => entry.id === "approve-global-evolve") ?? [];
32
+ if (matching.length !== 1 || !Array.isArray(matching[0]?.selected)) {
33
+ throw new Error("evolution approval returned no unique explicit decision");
34
+ }
35
+ const selected = [...new Set(matching[0]?.selected ?? [])];
36
+ const known = selected.filter((label) => label === "批准" || label === "拒绝");
37
+ if (known.length !== 1 || selected.length !== 1) {
38
+ throw new Error(`evolution approval returned an invalid decision: ${JSON.stringify(selected)}`);
39
+ }
40
+ return known[0] === "批准" ? "approved" : "declined";
41
+ }
42
+ /**
43
+ * Ask the user to approve a global edit. Throws when the service is missing,
44
+ * the user declines, or the question cannot be answered.
45
+ */
46
+ export async function requireGlobalApproval(ctx, agent, signal, what) {
47
+ if ((await requestScopeApproval(ctx, agent, signal, "global", what)) !== "approved") {
32
48
  throw new Error("global evolution edit rejected by the user");
33
49
  }
34
50
  }
package/lib/auto.d.ts CHANGED
@@ -1,8 +1,10 @@
1
+ import type { ScopeApprovalDecision } from "./approval.js";
1
2
  import type { Context } from "@deepseek-ai/cordis";
2
3
  import type { Agent } from "@deepseek-ai/dsh-agent";
3
4
  import type { HarnessState, RefinementEdit, RefinementProposal } from "./types.js";
4
5
  import type { EvolutionEngine } from "./service.js";
5
6
  import { type AutoRefineReason } from "./review.js";
7
+ import { type TurnSnapshot } from "./turn-snapshot.js";
6
8
  import type { PromotionPolicy } from "./promotion.js";
7
9
  import type { PlannerPrefixCacheMode } from "./prefix-cache.js";
8
10
  export interface AutoReviewConfig {
@@ -32,6 +34,8 @@ export interface AutoReviewConfig {
32
34
  * When absent, the review gate uses the agent's own provider/model.
33
35
  */
34
36
  reviewModel?: string;
37
+ /** Dedicated memory-agent writes to project/global require the normal human approval boundary. */
38
+ requireGlobalApproval?: boolean;
35
39
  /**
36
40
  * Prefix-cache routing for the gate input (see reviewAutoRefine).
37
41
  * Absent mode → auto-detect; absent budget → the default prefix budget.
@@ -74,6 +78,10 @@ export interface GateState {
74
78
  completedTurn: number;
75
79
  /** Latest turn whose snapshot acquisition was reserved. */
76
80
  lastSnapshotTurn: number;
81
+ /** Latest successful/no-op memory boundary, independent of later review/fate completion. */
82
+ memoryCheckpoint?: string;
83
+ /** Per-boundary/scope approval decisions retained until the memory phase settles. */
84
+ memoryDecisions: Record<string, ScopeApprovalDecision>;
77
85
  lastReviewAt: number;
78
86
  running: boolean;
79
87
  /**
@@ -145,6 +153,22 @@ export declare function loadGateHarnessView(engine: EvolutionEngine, sessionId:
145
153
  * it from runGate.
146
154
  */
147
155
  export declare function runGoalBlockedFate(ctx: Context, engine: EvolutionEngine, agent: Agent, config: AutoReviewConfig, state: GateState, _reason: AutoRefineReason, record: (entry: Omit<ReviewRecord, "timestamp">) => void): Promise<void>;
156
+ /**
157
+ * Dedicated ZCode-style memory phase. It shares the scheduler snapshot and
158
+ * snapshot with the general review, but owns its request loop, manifest and
159
+ * memory-only tool policy. Its phase-specific checkpoint advances after a
160
+ * successful/no-op memory outcome; later review/fate failures do not replay
161
+ * old memory decisions, while memory failures leave the boundary retryable.
162
+ */
163
+ export declare function runMemoryExtractionPhase(ctx: Context, engine: EvolutionEngine, agent: Agent, config: AutoReviewConfig, state: GateState, snapshot: TurnSnapshot, signal?: AbortSignal): Promise<void>;
164
+ /** True when the general planner returned only edits owned by the memory phase. */
165
+ export declare function isDedicatedMemoryOnlyProposal(proposal: RefinementProposal): boolean;
166
+ /**
167
+ * Remove memory edits from the general auto-review planner. The dedicated
168
+ * memory phase owns this responsibility and has already run against the same
169
+ * snapshot; accepting a second memory proposal would reintroduce duplicates.
170
+ */
171
+ export declare function stripDedicatedMemoryEdits(proposal: RefinementProposal): RefinementProposal;
148
172
  /**
149
173
  * Split a proposal into skill edits and everything else. Skill edits are the
150
174
  * governed part: they need explicit user consent before the gate applies
package/lib/auto.js CHANGED
@@ -30,14 +30,17 @@ 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
32
  import { isGateEnabled } from "./runtime.js";
33
- import { createReviewScheduler } from "./review-scheduler.js";
34
- import { captureTurnSnapshot } from "./turn-snapshot.js";
33
+ import { compareReviewCursors, createReviewScheduler } from "./review-scheduler.js";
34
+ import { captureTurnSnapshot, sliceTurnSnapshot } from "./turn-snapshot.js";
35
35
  import { captureAutoCase } from "./autocase.js";
36
+ import { applyMemoryExtractionProposal, buildMemoryManifest, runMemoryAgent, } from "./memory-agent.js";
36
37
  /** Turns a rejected skill candidate stays silent before being offered again. */
37
38
  export const SKILL_CONSULT_COOLDOWN_TURNS = 10;
38
39
  export function registerAutoReview(ctx, engine, config) {
39
40
  const perSession = new Map();
40
41
  const schedulers = new Map();
42
+ const captureTails = new Map();
43
+ const disposedSessions = new Set();
41
44
  const logger = ctx.logger("continual-evolve");
42
45
  const reviewsPath = join(engine.baseDir, "evolve", "reviews.jsonl");
43
46
  const defaultEnabled = config.enabledByDefault ?? true;
@@ -96,7 +99,7 @@ export function registerAutoReview(ctx, engine, config) {
96
99
  return scheduler;
97
100
  };
98
101
  const captureAndSchedule = (agent, state, reason) => {
99
- if (!isGateEnabled(engine.baseDir, defaultEnabled))
102
+ if (disposedSessions.has(agent.id) || !isGateEnabled(engine.baseDir, defaultEnabled))
100
103
  return;
101
104
  const turn = state.completedTurn;
102
105
  const previousSnapshotTurn = state.lastSnapshotTurn;
@@ -104,15 +107,20 @@ export function registerAutoReview(ctx, engine, config) {
104
107
  return;
105
108
  if (reason === "turn_snapshot")
106
109
  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) => {
110
+ const previousCapture = captureTails.get(agent.id) ?? Promise.resolve();
111
+ const captureTask = previousCapture.then(async () => {
112
+ if (disposedSessions.has(agent.id) || !isGateEnabled(engine.baseDir, defaultEnabled))
113
+ return;
114
+ const cursor = schedulerFor(agent, state).getCursor();
115
+ const snapshot = await captureTurnSnapshot(ctx, agent, {
116
+ turn,
117
+ reason,
118
+ ...(cursor !== undefined ? { cursor } : {}),
119
+ maxChars: config.maxInputChars,
120
+ });
121
+ if (!disposedSessions.has(agent.id))
122
+ schedulerFor(agent, state).schedule(snapshot);
123
+ }).catch((cause) => {
116
124
  const message = cause instanceof Error ? cause.message : String(cause);
117
125
  logger.warn(`auto-review snapshot failed for ${agent.id}: ${message}`);
118
126
  record({
@@ -126,6 +134,7 @@ export function registerAutoReview(ctx, engine, config) {
126
134
  state.lastSnapshotTurn = previousSnapshotTurn;
127
135
  }
128
136
  });
137
+ captureTails.set(agent.id, captureTask);
129
138
  };
130
139
  // DSH emits this awaited boundary when a turn is about to close. The idle
131
140
  // transition is the safe point at which to read the durable surface.
@@ -175,8 +184,10 @@ export function registerAutoReview(ctx, engine, config) {
175
184
  const agent = payload.agent;
176
185
  if (!agent)
177
186
  return;
187
+ disposedSessions.add(agent.id);
178
188
  schedulers.get(agent.id)?.shutdown();
179
189
  schedulers.delete(agent.id);
190
+ captureTails.delete(agent.id);
180
191
  perSession.delete(agent.id);
181
192
  });
182
193
  ctx.on("session/event", (session, event) => {
@@ -213,6 +224,7 @@ function stateFor(map, sessionId) {
213
224
  turns: 0,
214
225
  completedTurn: 0,
215
226
  lastSnapshotTurn: 0,
227
+ memoryDecisions: {},
216
228
  lastReviewAt: 0,
217
229
  running: false,
218
230
  skillRejects: new Map(),
@@ -224,6 +236,21 @@ function stateFor(map, sessionId) {
224
236
  }
225
237
  return state;
226
238
  }
239
+ function advanceMemoryCheckpoint(state, next) {
240
+ if (state.memoryCheckpoint === undefined) {
241
+ state.memoryCheckpoint = next;
242
+ return;
243
+ }
244
+ const order = compareReviewCursors(next, state.memoryCheckpoint);
245
+ if (order === undefined || order > 0)
246
+ state.memoryCheckpoint = next;
247
+ }
248
+ function rememberMemoryDecision(state, key, decision) {
249
+ state.memoryDecisions[key] = decision;
250
+ const keys = Object.keys(state.memoryDecisions);
251
+ while (keys.length > 128)
252
+ delete state.memoryDecisions[keys.shift() ?? ""];
253
+ }
227
254
  /**
228
255
  * The state view the gate and planner judge: the session's local entries
229
256
  * merged with the project and global stores, each entry carrying its real
@@ -255,6 +282,7 @@ async function runGate(ctx, engine, agent, config, state, snapshot, record, sign
255
282
  }
256
283
  state.running = true;
257
284
  try {
285
+ await runMemoryExtractionPhase(ctx, engine, agent, config, state, snapshot, signal);
258
286
  await runReviewPhase(ctx, engine, agent, config, state, snapshot, record, signal);
259
287
  // D3: a goal stuck in "blocked" for consecutive gate runs gets one
260
288
  // local-fate assessment (the pipeline below), so whatever led the
@@ -295,6 +323,84 @@ export async function runGoalBlockedFate(ctx, engine, agent, config, state, _rea
295
323
  logger.info(`auto-review goal-blocked trigger [${agent.id}]: ${config.goalBlockedWrapupTurns} consecutive blocked gate runs → local-fate assessment`);
296
324
  await runLocalFatePhase(ctx, engine, agent, config, state, "goal_blocked", record);
297
325
  }
326
+ /**
327
+ * Dedicated ZCode-style memory phase. It shares the scheduler snapshot and
328
+ * snapshot with the general review, but owns its request loop, manifest and
329
+ * memory-only tool policy. Its phase-specific checkpoint advances after a
330
+ * successful/no-op memory outcome; later review/fate failures do not replay
331
+ * old memory decisions, while memory failures leave the boundary retryable.
332
+ */
333
+ export async function runMemoryExtractionPhase(ctx, engine, agent, config, state, snapshot, signal) {
334
+ const memorySnapshot = state.memoryCheckpoint ? sliceTurnSnapshot(snapshot, state.memoryCheckpoint) : snapshot;
335
+ if (!memorySnapshot.eligible || !memorySnapshot.trajectory) {
336
+ advanceMemoryCheckpoint(state, snapshot.cursor);
337
+ ctx.logger("continual-evolve").info(`memory agent no-op (${snapshot.reason}) [${agent.id}]: no new eligible evidence after checkpoint`);
338
+ return;
339
+ }
340
+ const sessionId = agent.id;
341
+ const projectKey = snapshot.projectKey ?? projectKeyOf(agent);
342
+ const baselines = {
343
+ local: engine.load("local", sessionId),
344
+ global: engine.load("global", undefined),
345
+ ...(projectKey ? { project: engine.load("project", projectKey) } : {}),
346
+ };
347
+ const manifest = buildMemoryManifest(loadGateHarnessView(engine, sessionId, projectKey ? { projectKey } : undefined));
348
+ const overrideRoute = parseReviewModel(config.reviewModel, agent.options.provider);
349
+ const provider = overrideRoute?.provider ?? agent.options.provider;
350
+ const model = overrideRoute?.model ?? agent.options.model;
351
+ if (!provider || !model)
352
+ throw new Error("evolve: no provider/model route for the memory extraction agent");
353
+ const tokenUsage = {
354
+ baseDir: engine.baseDir,
355
+ sessionId,
356
+ retain: engine.retention.tokenUsage,
357
+ onError: (cause) => ctx
358
+ .logger("continual-evolve")
359
+ .warn(`token-usage ledger failed for ${sessionId}: ${cause instanceof Error ? cause.message : String(cause)}`),
360
+ };
361
+ const run = await runMemoryAgent(ctx, {
362
+ provider,
363
+ model,
364
+ manifest,
365
+ trajectory: memorySnapshot.trajectory,
366
+ trajectoryEvents: memorySnapshot.events,
367
+ maxOutputTokens: config.budgetTokens,
368
+ ...(signal ? { signal } : {}),
369
+ tokenUsage,
370
+ ...((config.prefixCacheMode !== undefined || config.prefixMaxChars !== undefined
371
+ ? {
372
+ prefixCache: {
373
+ ...(config.prefixCacheMode !== undefined ? { mode: config.prefixCacheMode } : {}),
374
+ ...(config.prefixMaxChars !== undefined ? { maxChars: config.prefixMaxChars } : {}),
375
+ },
376
+ }
377
+ : {})),
378
+ });
379
+ if (run.proposal.edits.length === 0) {
380
+ advanceMemoryCheckpoint(state, memorySnapshot.cursor);
381
+ ctx.logger("continual-evolve").info(`memory agent no-op (${snapshot.reason}) [${sessionId}] after ${run.turns} turn(s): ${run.proposal.rationale}`);
382
+ return;
383
+ }
384
+ const source = { sessionId, ...(memorySnapshot.sourceSeqs.length > 0 ? { seqs: [...memorySnapshot.sourceSeqs] } : {}) };
385
+ const application = await applyMemoryExtractionProposal(ctx, engine, run.proposal, {
386
+ agent,
387
+ baselines,
388
+ ...(projectKey ? { projectKey } : {}),
389
+ requireApproval: config.requireGlobalApproval ?? true,
390
+ source,
391
+ decisionCursor: memorySnapshot.cursor,
392
+ scopeDecisions: state.memoryDecisions,
393
+ onScopeDecision: (key, decision) => rememberMemoryDecision(state, key, decision),
394
+ ...(signal ? { signal } : {}),
395
+ });
396
+ for (const result of application.results) {
397
+ emitEvolveComplete(engine.baseDir, buildEvolveCompleteEvent(result, `memory_agent:${snapshot.reason}`, sessionId), config.reviewsRetain ?? DEFAULT_REVIEWS_RETAIN);
398
+ }
399
+ state.memoryCheckpoint = memorySnapshot.cursor;
400
+ const applied = application.results.reduce((count, result) => count + result.appliedEdits.filter((edit) => edit.applied).length, 0);
401
+ const declined = application.declinedScopes.length > 0 ? `; declined scopes=${application.declinedScopes.join(",")}` : "";
402
+ 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}`);
403
+ }
298
404
  async function runReviewPhase(ctx, engine, agent, config, state, snapshot, record, signal) {
299
405
  const sessionId = agent.id;
300
406
  const reason = snapshot.reason;
@@ -345,7 +451,7 @@ async function runReviewPhase(ctx, engine, agent, config, state, snapshot, recor
345
451
  record({ sessionId, reason, turnsSinceLastReview, outcome: "declined", rationale: review.rationale });
346
452
  return;
347
453
  }
348
- const proposal = await planWithLlm(ctx, {
454
+ const plannedProposal = await planWithLlm(ctx, {
349
455
  agent,
350
456
  state: harnessState,
351
457
  history,
@@ -367,6 +473,8 @@ async function runReviewPhase(ctx, engine, agent, config, state, snapshot, recor
367
473
  }
368
474
  : {})),
369
475
  });
476
+ const proposal = stripDedicatedMemoryEdits(plannedProposal);
477
+ const memoryOnlyPlan = isDedicatedMemoryOnlyProposal(plannedProposal);
370
478
  // Skills are governed resources: an auto-created skill is OFFERED to the
371
479
  // user for a decision (固化/不固化) before it lands — the gate never
372
480
  // writes a skill silently. Without consent the skill edits are withheld
@@ -385,7 +493,7 @@ async function runReviewPhase(ctx, engine, agent, config, state, snapshot, recor
385
493
  logger.info(`auto-review declined (${reason}) [${sessionId}]: no consented edits${withheld} — ${review.rationale}`);
386
494
  // P1 auto-case capture: an attempted evolution that never landed is a
387
495
  // regression asset. Contained — capture failure must not disturb the gate.
388
- if (config.autoCase) {
496
+ if (config.autoCase && !memoryOnlyPlan) {
389
497
  try {
390
498
  const captured = captureAutoCase({
391
499
  baseDir: engine.baseDir,
@@ -422,6 +530,25 @@ async function runReviewPhase(ctx, engine, agent, config, state, snapshot, recor
422
530
  notifyAutoReview(ctx, agent, result, turnsSinceLastReview);
423
531
  }
424
532
  }
533
+ /** True when the general planner returned only edits owned by the memory phase. */
534
+ export function isDedicatedMemoryOnlyProposal(proposal) {
535
+ return proposal.edits.length > 0 && proposal.edits.every((edit) => edit.kind === "memory");
536
+ }
537
+ /**
538
+ * Remove memory edits from the general auto-review planner. The dedicated
539
+ * memory phase owns this responsibility and has already run against the same
540
+ * snapshot; accepting a second memory proposal would reintroduce duplicates.
541
+ */
542
+ export function stripDedicatedMemoryEdits(proposal) {
543
+ const edits = proposal.edits.filter((edit) => edit.kind !== "memory");
544
+ if (edits.length === proposal.edits.length)
545
+ return proposal;
546
+ return {
547
+ ...proposal,
548
+ edits,
549
+ summary: `${proposal.summary} (memory edits owned by dedicated extractor)`,
550
+ };
551
+ }
425
552
  /**
426
553
  * Split a proposal into skill edits and everything else. Skill edits are the
427
554
  * governed part: they need explicit user consent before the gate applies
package/lib/index.js CHANGED
@@ -3,9 +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 review gate. The listener is always registered, while its
7
- * effective state is controlled by the runtime switch. Successful turns feed
8
- * incremental snapshots through a per-session serial scheduler.
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.
9
10
  */
10
11
  import { join } from "node:path";
11
12
  import z from "@deepseek-ai/schemastery";
@@ -194,11 +195,12 @@ export function apply(ctx, config) {
194
195
  rubricKey,
195
196
  ...(config.historyRetain?.reviews !== undefined ? { reviewsRetain: config.historyRetain.reviews } : {}),
196
197
  ...(config.reviewModel ? { reviewModel: config.reviewModel } : {}),
198
+ requireGlobalApproval: config.requireGlobalApproval ?? true,
197
199
  ...(config.plannerPrefixCache ? { prefixCacheMode: config.plannerPrefixCache } : {}),
198
200
  ...(config.plannerPrefixMaxChars !== undefined ? { prefixMaxChars: config.plannerPrefixMaxChars } : {}),
199
201
  });
200
202
  const runtimeDefault = config.autoReview ?? false;
201
- ctx.logger("continual-evolve").info(`continual-evolve auto-review registered (default ${runtimeDefault ? "on" : "off"}; successful-turn snapshots; local-fate ${config.localFate ?? true ? "on" : "off"} every ${config.fateIntervalTurns ?? config.reviewIntervalTurns ?? 6} turns)`);
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)`);
202
204
  ctx.logger("continual-evolve").info(`continual-evolve mounted (baseDir=${baseDir})`);
203
205
  }
204
206
  //# sourceMappingURL=index.js.map