dsh-continual-evolve 0.7.6 → 0.9.0
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 +20 -7
- package/README.zh.md +16 -7
- package/lib/auto.d.ts +31 -2
- package/lib/auto.js +124 -6
- package/lib/benchmark-command.js +1 -1
- package/lib/command.js +93 -1
- package/lib/fate.js +10 -10
- package/lib/index.d.ts +10 -0
- package/lib/index.js +6 -0
- package/lib/memory-agent.d.ts +8 -1
- package/lib/memory-agent.js +13 -0
- package/lib/memory-benchmark.d.ts +76 -0
- package/lib/memory-benchmark.js +170 -0
- package/lib/notify.d.ts +27 -0
- package/lib/notify.js +54 -0
- package/lib/planner.d.ts +1 -1
- package/lib/planner.js +7 -1
- package/lib/projection.d.ts +22 -0
- package/lib/projection.js +112 -0
- package/lib/recall.d.ts +85 -0
- package/lib/recall.js +142 -0
- package/lib/review-scheduler.js +3 -4
- package/lib/review.d.ts +1 -1
- package/lib/review.js +8 -3
- package/lib/service.js +14 -0
- package/lib/skill-render.js +2 -1
- package/lib/skillquality.d.ts +7 -2
- package/lib/skillquality.js +12 -3
- package/lib/tool.js +40 -0
- package/lib/types.d.ts +9 -11
- package/lib/types.js +14 -2
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
[](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
[](package.json)
|
|
10
|
-
[]()
|
|
11
|
+
[]()
|
|
12
|
+
[]()
|
|
13
|
+
[]()
|
|
11
14
|
|
|
12
15
|
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
16
|
|
|
@@ -20,6 +23,7 @@ Agents accumulate reusable experience (repeated failures, durable facts, reusabl
|
|
|
20
23
|
- **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
24
|
- **Typed one-fact memories**: every memory entry carries a recall type (`user | feedback | project | reference`); pitfalls (`feedback`) must include Why + How to apply
|
|
22
25
|
- **Dedicated background memory agent**: 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
|
|
26
|
+
- **Memory recall, projection, and receipts**: `evolve_recall` reads back full memory content by query/kind/scope/type; every memory apply also materializes a readable `MEMORY.md` index plus one fact file per entry; each extraction lands a unified audit receipt (no-op/applied/declined with duration and turn stats) and only applied outcomes notify the session
|
|
23
27
|
- **Deterministic rollback**: inverse edits generated from applied results — no LLM re-guessing
|
|
24
28
|
- **Benchmark loop**: candidate refinements are evaluated against frozen cases by a separate scorer before acceptance (rubric encrypted at rest)
|
|
25
29
|
- **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
|
|
@@ -56,6 +60,9 @@ Commands (in-session):
|
|
|
56
60
|
| `/evolve plan [msg]` | run the LLM planner against the store |
|
|
57
61
|
| `/evolve wrapup` | assess this session's local entries: promote / archive / keep |
|
|
58
62
|
| `/evolve archive · unarchive · demote <id>` | hide from injection (data kept, restorable) — `demote` targets global noise |
|
|
63
|
+
| `/evolve recall [scope] <query…>` | targeted memory recall: full content with version, source, and staleness |
|
|
64
|
+
| `/evolve remember <type> [scope] <text…>` | immediately persist one typed memory (`user|feedback|project|reference`) |
|
|
65
|
+
| `/evolve forget [scope] <query…>` | locate one memory and archive it (restorable); ambiguous queries only list |
|
|
59
66
|
| `/evolve consolidate [apply] [merge]` | report (or apply) one batch archive of conflict-hinted + stale zero-use global entries; `merge` folds near-duplicate content into the survivors |
|
|
60
67
|
| `/evolve failures` | aggregated failure classes (gate + benchmark) |
|
|
61
68
|
| `/evolve log [tail N] [session <id>]` | plugin log |
|
|
@@ -66,13 +73,13 @@ Commands (in-session):
|
|
|
66
73
|
| `/evolve pause · resume · status` | pause/resume the auto-review gate (manual tools keep working), gate state |
|
|
67
74
|
| `/evolve usage` | per-entry injection counts + exact provider-reported tokens for direct memory/review/planner/wrapup/fate calls (benchmark host subagents excluded) |
|
|
68
75
|
|
|
69
|
-
Model tools: `evolve_list / add / update / delete / rollback` (`evolve_delete` takes `id` or a batch `ids` array — one refinement, one approval).
|
|
76
|
+
Model tools: `evolve_list / add / update / delete / rollback / recall` (`evolve_delete` takes `id` or a batch `ids` array — one refinement, one approval; `evolve_recall` filters by query, kinds, scopes, memory types, and limit, and returns full content with version, source, and staleness).
|
|
70
77
|
|
|
71
78
|
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.
|
|
72
79
|
|
|
73
80
|
`/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.
|
|
74
81
|
|
|
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.
|
|
82
|
+
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_recall` (targeted) or `evolve_list`. Every memory apply also refreshes a readable `MEMORY.md` index plus one fact file per entry in the store directory. Empty store = zero injected tokens.
|
|
76
83
|
|
|
77
84
|
## Configuration
|
|
78
85
|
|
|
@@ -81,6 +88,7 @@ Injection shape: prompt notes and delegation specs inject with content (≤6/kin
|
|
|
81
88
|
| `baseDir` | resolved DSH home | root for the `evolve/` stores |
|
|
82
89
|
| `autoReview` | `false` | initial Memory Agent default when no `evolve/runtime.json` exists yet; the listener is always registered, so this is not a registration gate |
|
|
83
90
|
| `memoryMinUserWords` | `3` | ZCode-style minimum lexical words in one direct user text part; uses CJK-aware segmentation |
|
|
91
|
+
| `sessionCloseDrainMs` | `15000` | session-close bounded drain for in-flight extraction in ms (`0` aborts immediately) |
|
|
84
92
|
| `reviewIntervalTurns` | `6` | legacy local-fate cadence fallback; successful-turn review no longer waits for this interval |
|
|
85
93
|
| `maxReviewInputChars` | `40000` | trajectory slice handed to the gate |
|
|
86
94
|
| `reviewBudgetTokens` | `4096` | output budget for the gate call |
|
|
@@ -124,22 +132,27 @@ phases are not reachable from this listener. The memory trigger follows ZCode's
|
|
|
124
132
|
lightweight eligibility: direct user text must contain at least
|
|
125
133
|
`memoryMinUserWords` lexical words (CJK-aware segmentation), while
|
|
126
134
|
empty/internal/direct-memory-write snapshots are skipped; compaction does not
|
|
127
|
-
add a separate memory-only trigger.
|
|
135
|
+
add a separate memory-only trigger. Every extraction writes a unified audit
|
|
136
|
+
receipt (`noop`/`applied`/`declined` with duration and turn/search stats) to
|
|
137
|
+
`reviews.jsonl`; only applied outcomes queue a visible follow-up, and a
|
|
138
|
+
closing session lets in-flight extraction settle up to `sessionCloseDrainMs`
|
|
139
|
+
before aborting.
|
|
128
140
|
|
|
129
141
|
## Development
|
|
130
142
|
|
|
131
143
|
```bash
|
|
132
144
|
pnpm install && pnpm build # deps + tsc -> lib/
|
|
133
|
-
pnpm test # vitest (
|
|
145
|
+
pnpm test # vitest (1031 tests)
|
|
134
146
|
pnpm test:coverage # v8 coverage, thresholds enforced in CI
|
|
147
|
+
pnpm coverage:gaps # locate uncovered lines per file (read-only)
|
|
135
148
|
pnpm lint # oxlint src test
|
|
136
149
|
```
|
|
137
150
|
|
|
138
151
|
Project layout:
|
|
139
152
|
|
|
140
153
|
```
|
|
141
|
-
├── src/ # engine, tools, commands, memory agent, gate, fate, benchmark, injection + token usage…
|
|
142
|
-
├── test/ # vitest suites (
|
|
154
|
+
├── src/ # engine, tools, commands, memory agent, recall, projection, gate, fate, benchmark, injection + token usage…
|
|
155
|
+
├── test/ # vitest suites (50 files)
|
|
143
156
|
├── lib/ # build output (tsc)
|
|
144
157
|
├── docs/
|
|
145
158
|
│ ├── design.md # full design doc (hardening matrix)
|
package/README.zh.md
CHANGED
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
[](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
[](package.json)
|
|
10
|
-
[]()
|
|
11
|
+
[]()
|
|
12
|
+
[]()
|
|
13
|
+
[]()
|
|
11
14
|
|
|
12
15
|
[DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(`dsh`)的持续自进化插件:一套**版本化、可审计、可回滚**的 harness 状态层——提示词补充、记忆、技能、子代理规格——从会话轨迹中沉淀而来。
|
|
13
16
|
|
|
@@ -20,6 +23,7 @@ Agent 在每个会话里积累可复用经验(重复失败、持久事实、
|
|
|
20
23
|
- **三作用域**与合并语义(global < project < local):**local** 本会话暂存、**project** 本项目跨会话库、**global** 跨项目——配合机械化晋升守卫,只有可携带、有分量、非重复的知识才能进全局
|
|
21
24
|
- **类型化单条记忆**:每条 memory 带召回类型(`user | feedback | project | reference`);踩坑(`feedback`)必须含 Why + How to apply
|
|
22
25
|
- **专用后台记忆 Agent**:成功回合通过机械 eligibility 后,有界 ZCode 式 loop 检索冻结的 memory manifest,并经闭集工具提出纯 memory 编辑;不能调用 Agent、MCP、网络,也不能写源码。该 listener 不运行通用 review/planner/fate
|
|
26
|
+
- **记忆召回、投影与回执**:`evolve_recall` 按 query/kind/scope/type 精确读回记忆全文;每次 memory 落盘同时生成可读的 `MEMORY.md` 索引 + 单事实文件;每次提取留统一审计回执(no-op/applied/declined + 耗时与轮次统计),只有实际沉淀才通知会话
|
|
23
27
|
- **确定性回滚**:逆操作编辑由已应用结果生成——不靠 LLM 重新猜测
|
|
24
28
|
- **benchmark 闭环**:候选沉淀先经冻结用例 + 独立评分者评估再接受(rubric 加密落盘)
|
|
25
29
|
- **store 卫生**:`/evolve consolidate` 把写入时冲突提示与零使用陈旧条目变成一次批准、完全可逆的批量归档——加 `merge` 可将近重复内容并入幸存原条目
|
|
@@ -56,6 +60,9 @@ dsh plugin add ZK-Andy/dsh-continual-evolve
|
|
|
56
60
|
| `/evolve plan [msg]` | 对 store 运行 LLM 规划器 |
|
|
57
61
|
| `/evolve wrapup` | 收尾本会话 local 条目:晋升 / 归档 / 保留 |
|
|
58
62
|
| `/evolve archive · unarchive · demote <id>` | 从注入中隐藏(数据保留可恢复)——`demote` 针对全局噪声 |
|
|
63
|
+
| `/evolve recall [scope] <query…>` | 定向记忆召回:全文 + 版本 + 来源 + 过时信号 |
|
|
64
|
+
| `/evolve remember <type> [scope] <text…>` | 立即持久化一条类型化记忆(`user|feedback|project|reference`) |
|
|
65
|
+
| `/evolve forget [scope] <query…>` | 按 query 定位一条记忆并归档(可恢复);多义只列候选不写 |
|
|
59
66
|
| `/evolve consolidate [apply] [merge]` | 报告(或应用)冲突提示 + 零使用陈旧全局条目的批量归档;`merge` 将近重复内容并入幸存原条目 |
|
|
60
67
|
| `/evolve failures` | 失败类聚合(门禁 + benchmark) |
|
|
61
68
|
| `/evolve log [tail N] [session <id>]` | 插件日志 |
|
|
@@ -66,13 +73,13 @@ dsh plugin add ZK-Andy/dsh-continual-evolve
|
|
|
66
73
|
| `/evolve pause · resume · status` | 暂停/恢复自动门禁(手动工具不受影响)、门禁状态 |
|
|
67
74
|
| `/evolve usage` | 每条目注入次数 + memory/review/planner/wrapup/fate 直属调用的 provider 精确 token(不含 benchmark 宿主子代理) |
|
|
68
75
|
|
|
69
|
-
模型工具:`evolve_list / add / update / delete / rollback`(`evolve_delete` 支持 `id` 或批量 `ids` 数组——一次 refinement
|
|
76
|
+
模型工具:`evolve_list / add / update / delete / rollback / recall`(`evolve_delete` 支持 `id` 或批量 `ids` 数组——一次 refinement、一次审批;`evolve_recall` 按 query/kind/scope/memoryType/limit 过滤,返回全文 + 版本 + 来源 + 过时信号)。
|
|
70
77
|
|
|
71
78
|
第三方消费:每次进化落地(门禁或手动)都会向 `reviews.jsonl` 追加结构化 `evolve_complete` 事件(shape 见 `src/evolve-event.ts`),与人类可读的审计记录并存。
|
|
72
79
|
|
|
73
80
|
`/evolve usage` 还会读取 `evolve/token-usage.jsonl`:插件直属 memory Agent、review、planner、手动 wrapup 与自动 fate 调用的 provider 精确 input/cache/output/total token。报告只覆盖保留尾部而非终身累计,单独显示 provider 未返回 usage 的调用,并明确排除宿主 benchmark 子代理、其 agent-loop 调用与逐条 memory 注入归因。
|
|
74
81
|
|
|
75
|
-
注入形态:prompt 补充与委派规格带内容注入(每 kind ≤6 条 × 180 字符,按相关性排序)。memory/skill 以按相关性排序的目录索引出现(`[memory:type:id] 标题`钩子,15 行封顶 + 折叠计数行)——全文经 `evolve_list`
|
|
82
|
+
注入形态:prompt 补充与委派规格带内容注入(每 kind ≤6 条 × 180 字符,按相关性排序)。memory/skill 以按相关性排序的目录索引出现(`[memory:type:id] 标题`钩子,15 行封顶 + 折叠计数行)——全文经 `evolve_recall`(精确)或 `evolve_list` 获取。每次 memory 落盘同时刷新 store 目录下的可读 `MEMORY.md` 索引 + 单事实文件。空 store = 零注入 token。
|
|
76
83
|
|
|
77
84
|
## 配置
|
|
78
85
|
|
|
@@ -81,6 +88,7 @@ dsh plugin add ZK-Andy/dsh-continual-evolve
|
|
|
81
88
|
| `baseDir` | 解析后的 DSH home | `evolve/` 存储根目录 |
|
|
82
89
|
| `autoReview` | `false` | 没有 `evolve/runtime.json` 时 Memory Agent 的初始默认;监听器始终注册,因此它不是注册门 |
|
|
83
90
|
| `memoryMinUserWords` | `3` | ZCode 风格:单个直接用户文本至少包含的词数;使用 CJK 分词 |
|
|
91
|
+
| `sessionCloseDrainMs` | `15000` | 会话关闭时在途提取的有界 drain(毫秒,`0` 立即 abort) |
|
|
84
92
|
| `reviewIntervalTurns` | `6` | local-fate 的兼容节奏;成功回合 review 不再等待这个间隔 |
|
|
85
93
|
| `maxReviewInputChars` | `40000` | 交给门禁的轨迹切片 |
|
|
86
94
|
| `reviewBudgetTokens` | `4096` | 门禁调用输出预算 |
|
|
@@ -112,22 +120,23 @@ profile patch 示例:
|
|
|
112
120
|
reviewIntervalTurns: 6
|
|
113
121
|
```
|
|
114
122
|
|
|
115
|
-
即使 `autoReview` 为 `false`,Memory Agent 监听器也会注册——`autoReview` 只提供初始默认值,装完即用,无需改 profile。使用 `/evolve resume` 立即开启成功回合 snapshot,使用 `/evolve pause` 抑制新 snapshot 与模型调用,使用 `/evolve status` 查看配置默认值与运行时状态。开关保存在 `evolve/runtime.json`;手动 `evolve_*` 工具和 `/evolve` 命令不受暂停影响。自动运行只有 Memory Agent:通用 review/planner、prompt/skill 写入与 local fate 都不由该 listener 触达。Memory 触发采用 ZCode 的轻量 eligibility:单个直接用户文本至少包含 `memoryMinUserWords` 个词(使用 CJK 分词);空增量、内部 Agent、直接 memory 写入会跳过,压缩不会额外触发 memory-only
|
|
123
|
+
即使 `autoReview` 为 `false`,Memory Agent 监听器也会注册——`autoReview` 只提供初始默认值,装完即用,无需改 profile。使用 `/evolve resume` 立即开启成功回合 snapshot,使用 `/evolve pause` 抑制新 snapshot 与模型调用,使用 `/evolve status` 查看配置默认值与运行时状态。开关保存在 `evolve/runtime.json`;手动 `evolve_*` 工具和 `/evolve` 命令不受暂停影响。自动运行只有 Memory Agent:通用 review/planner、prompt/skill 写入与 local fate 都不由该 listener 触达。Memory 触发采用 ZCode 的轻量 eligibility:单个直接用户文本至少包含 `memoryMinUserWords` 个词(使用 CJK 分词);空增量、内部 Agent、直接 memory 写入会跳过,压缩不会额外触发 memory-only 管线。每次提取都在 `reviews.jsonl` 留统一审计回执(`noop`/`applied`/`declined` + 耗时与轮次/检索统计);只有实际沉淀才排队前台通知,关闭的会话让在途提取在 `sessionCloseDrainMs` 内收尾再 abort。
|
|
116
124
|
|
|
117
125
|
## 开发
|
|
118
126
|
|
|
119
127
|
```bash
|
|
120
128
|
pnpm install && pnpm build # 依赖 + tsc -> lib/
|
|
121
|
-
pnpm test # vitest(
|
|
129
|
+
pnpm test # vitest(1031 例)
|
|
122
130
|
pnpm test:coverage # v8 覆盖率,CI 强制阈值
|
|
131
|
+
pnpm coverage:gaps # 定位各文件未覆盖行(只读)
|
|
123
132
|
pnpm lint # oxlint src test
|
|
124
133
|
```
|
|
125
134
|
|
|
126
135
|
目录结构:
|
|
127
136
|
|
|
128
137
|
```
|
|
129
|
-
├── src/ # 引擎、工具、命令、memory Agent
|
|
130
|
-
├── test/ # vitest 测试套件(
|
|
138
|
+
├── src/ # 引擎、工具、命令、memory Agent、召回、投影、门禁、fate、benchmark、注入 + token 用量…
|
|
139
|
+
├── test/ # vitest 测试套件(50 个文件)
|
|
131
140
|
├── lib/ # 构建产物(tsc)
|
|
132
141
|
├── docs/
|
|
133
142
|
│ ├── design.md # 完整设计文档(硬化矩阵)
|
package/lib/auto.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { Agent } from "@deepseek-ai/dsh-agent";
|
|
|
4
4
|
import type { HarnessState, RefinementEdit, RefinementProposal } from "./types.js";
|
|
5
5
|
import type { EvolutionEngine } from "./service.js";
|
|
6
6
|
import { type AutoRefineReason } from "./review.js";
|
|
7
|
+
import { type ReviewScheduler } from "./review-scheduler.js";
|
|
7
8
|
import { type TurnSnapshot } from "./turn-snapshot.js";
|
|
8
9
|
import type { PromotionPolicy } from "./promotion.js";
|
|
9
10
|
import type { PlannerPrefixCacheMode } from "./prefix-cache.js";
|
|
@@ -78,6 +79,12 @@ export interface AutoReviewConfig {
|
|
|
78
79
|
* window, not the full past).
|
|
79
80
|
*/
|
|
80
81
|
reviewsRetain?: number;
|
|
82
|
+
/**
|
|
83
|
+
* Session-close bounded drain (P1): how long a disposed session's
|
|
84
|
+
* in-flight extraction may settle before its owner signal aborts.
|
|
85
|
+
* Absent → 15s; 0 restores the legacy immediate abort.
|
|
86
|
+
*/
|
|
87
|
+
sessionCloseDrainMs?: number;
|
|
81
88
|
}
|
|
82
89
|
export interface GateState {
|
|
83
90
|
turns: number;
|
|
@@ -114,14 +121,36 @@ export interface GateState {
|
|
|
114
121
|
}
|
|
115
122
|
/** Turns a rejected skill candidate stays silent before being offered again. */
|
|
116
123
|
export declare const SKILL_CONSULT_COOLDOWN_TURNS = 10;
|
|
124
|
+
/** Default session-close drain budget: an in-flight extraction gets 15s to settle. */
|
|
125
|
+
export declare const SESSION_CLOSE_DRAIN_MS_DEFAULT = 15000;
|
|
126
|
+
/**
|
|
127
|
+
* Resolve the session-close drain budget: absent/invalid → default, 0 →
|
|
128
|
+
* immediate abort (legacy). Negative or non-finite values fail back to the
|
|
129
|
+
* default rather than silently disabling the drain.
|
|
130
|
+
*/
|
|
131
|
+
export declare function resolveSessionCloseDrainMs(raw?: number): number;
|
|
132
|
+
/**
|
|
133
|
+
* Bounded session-close drain for one scheduler: wait for in-flight work up
|
|
134
|
+
* to drainMs, then abort the owner signal and drop pending captures. Never
|
|
135
|
+
* rejects — disposal must not hang the host on a stuck model call, and the
|
|
136
|
+
* abort below is idempotent when the drain already settled.
|
|
137
|
+
*/
|
|
138
|
+
export declare function drainSchedulerOnDispose(scheduler: Pick<ReviewScheduler<TurnSnapshot>, "drain" | "shutdown">, drainMs: number): Promise<void>;
|
|
117
139
|
export interface ReviewRecord {
|
|
118
140
|
timestamp: string;
|
|
119
141
|
sessionId: string;
|
|
120
142
|
reason: AutoRefineReason;
|
|
121
143
|
turnsSinceLastReview: number;
|
|
122
|
-
outcome: "approved" | "declined" | "failed" | "assessed" | "deferred" | "skipped" | "armed";
|
|
144
|
+
outcome: "approved" | "declined" | "failed" | "assessed" | "deferred" | "skipped" | "armed" | "applied" | "noop";
|
|
123
145
|
rationale?: string;
|
|
124
146
|
refinementId?: string;
|
|
147
|
+
/** P1 extraction stats (§6.7): wall time of the memory phase. */
|
|
148
|
+
durationMs?: number;
|
|
149
|
+
/** Extractor loop turns / manifest searches behind this row. */
|
|
150
|
+
memoryTurns?: number;
|
|
151
|
+
memorySearches?: number;
|
|
152
|
+
/** Applied edit count carried by an applied row. */
|
|
153
|
+
appliedEdits?: number;
|
|
125
154
|
}
|
|
126
155
|
export declare function registerAutoReview(ctx: Context, engine: EvolutionEngine, config: AutoReviewConfig): void;
|
|
127
156
|
/**
|
|
@@ -167,7 +196,7 @@ export declare function runGoalBlockedFate(ctx: Context, engine: EvolutionEngine
|
|
|
167
196
|
* successful/no-op memory outcome; later review/fate failures do not replay
|
|
168
197
|
* old memory decisions, while memory failures leave the boundary retryable.
|
|
169
198
|
*/
|
|
170
|
-
export declare function runMemoryExtractionPhase(ctx: Context, engine: EvolutionEngine, agent: Agent, config: AutoReviewConfig, state: GateState, snapshot: TurnSnapshot, signal?: AbortSignal): Promise<void>;
|
|
199
|
+
export declare function runMemoryExtractionPhase(ctx: Context, engine: EvolutionEngine, agent: Agent, config: AutoReviewConfig, state: GateState, snapshot: TurnSnapshot, record: (entry: Omit<ReviewRecord, "timestamp">) => void, signal?: AbortSignal): Promise<void>;
|
|
171
200
|
/** True when the general planner returned only edits owned by the memory phase. */
|
|
172
201
|
export declare function isDedicatedMemoryOnlyProposal(proposal: RefinementProposal): boolean;
|
|
173
202
|
/**
|
package/lib/auto.js
CHANGED
|
@@ -23,7 +23,7 @@ import { slug } from "./types.js";
|
|
|
23
23
|
import { planWithLlm } from "./planner.js";
|
|
24
24
|
import { reviewAutoRefine } from "./review.js";
|
|
25
25
|
import { goalServiceOf } from "./goal.js";
|
|
26
|
-
import { notifyAutoReview } from "./notify.js";
|
|
26
|
+
import { notifyAutoReview, notifyMemoryExtraction } from "./notify.js";
|
|
27
27
|
import { runLocalFatePhase } from "./fate.js";
|
|
28
28
|
import { mergeHarnessStates } from "./state.js";
|
|
29
29
|
import { projectKeyOf } from "./project.js";
|
|
@@ -38,6 +38,52 @@ import { applyMemoryExtractionProposal, buildMemoryManifest, runMemoryAgent, } f
|
|
|
38
38
|
import { asLlmSessionId } from "./llm-text.js";
|
|
39
39
|
/** Turns a rejected skill candidate stays silent before being offered again. */
|
|
40
40
|
export const SKILL_CONSULT_COOLDOWN_TURNS = 10;
|
|
41
|
+
/** Default session-close drain budget: an in-flight extraction gets 15s to settle. */
|
|
42
|
+
export const SESSION_CLOSE_DRAIN_MS_DEFAULT = 15000;
|
|
43
|
+
/**
|
|
44
|
+
* Resolve the session-close drain budget: absent/invalid → default, 0 →
|
|
45
|
+
* immediate abort (legacy). Negative or non-finite values fail back to the
|
|
46
|
+
* default rather than silently disabling the drain.
|
|
47
|
+
*/
|
|
48
|
+
export function resolveSessionCloseDrainMs(raw) {
|
|
49
|
+
if (raw === undefined)
|
|
50
|
+
return SESSION_CLOSE_DRAIN_MS_DEFAULT;
|
|
51
|
+
if (typeof raw !== "number" || !Number.isFinite(raw) || raw < 0)
|
|
52
|
+
return SESSION_CLOSE_DRAIN_MS_DEFAULT;
|
|
53
|
+
return Math.floor(raw);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Bounded session-close drain for one scheduler: wait for in-flight work up
|
|
57
|
+
* to drainMs, then abort the owner signal and drop pending captures. Never
|
|
58
|
+
* rejects — disposal must not hang the host on a stuck model call, and the
|
|
59
|
+
* abort below is idempotent when the drain already settled.
|
|
60
|
+
*/
|
|
61
|
+
export function drainSchedulerOnDispose(scheduler, drainMs) {
|
|
62
|
+
if (drainMs <= 0) {
|
|
63
|
+
try {
|
|
64
|
+
scheduler.shutdown();
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// shutdown is flag-setting plus abort: nothing observable to do
|
|
68
|
+
}
|
|
69
|
+
return Promise.resolve();
|
|
70
|
+
}
|
|
71
|
+
let timer;
|
|
72
|
+
const timeout = new Promise((resolve) => {
|
|
73
|
+
timer = setTimeout(resolve, drainMs);
|
|
74
|
+
});
|
|
75
|
+
const settle = () => {
|
|
76
|
+
if (timer !== undefined)
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
try {
|
|
79
|
+
scheduler.shutdown();
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// same as above — the abort is best-effort by design
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
return Promise.race([scheduler.drain(), timeout]).then(settle, settle);
|
|
86
|
+
}
|
|
41
87
|
export function registerAutoReview(ctx, engine, config) {
|
|
42
88
|
const perSession = new Map();
|
|
43
89
|
const schedulers = new Map();
|
|
@@ -82,7 +128,7 @@ export function registerAutoReview(ctx, engine, config) {
|
|
|
82
128
|
}
|
|
83
129
|
try {
|
|
84
130
|
if (config.memoryOnly) {
|
|
85
|
-
await runMemoryExtractionPhase(ctx, engine, agent, config, state, snapshot, signal);
|
|
131
|
+
await runMemoryExtractionPhase(ctx, engine, agent, config, state, snapshot, record, signal);
|
|
86
132
|
}
|
|
87
133
|
else {
|
|
88
134
|
await runGate(ctx, engine, agent, config, state, snapshot, record, signal);
|
|
@@ -193,10 +239,17 @@ export function registerAutoReview(ctx, engine, config) {
|
|
|
193
239
|
if (!agent)
|
|
194
240
|
return;
|
|
195
241
|
disposedSessions.add(agent.id);
|
|
196
|
-
schedulers.get(agent.id)
|
|
242
|
+
const scheduler = schedulers.get(agent.id);
|
|
197
243
|
schedulers.delete(agent.id);
|
|
198
244
|
captureTails.delete(agent.id);
|
|
199
245
|
perSession.delete(agent.id);
|
|
246
|
+
if (scheduler) {
|
|
247
|
+
// Bounded drain (P1): an in-flight extraction gets drainMs to
|
|
248
|
+
// settle so its result is not lost on session close; afterwards
|
|
249
|
+
// the owner signal aborts. Fire-and-forget — disposal must never
|
|
250
|
+
// hang the host, and the helper never rejects.
|
|
251
|
+
void drainSchedulerOnDispose(scheduler, resolveSessionCloseDrainMs(config.sessionCloseDrainMs));
|
|
252
|
+
}
|
|
200
253
|
});
|
|
201
254
|
ctx.on("session/event", (session, event) => {
|
|
202
255
|
// ZCode schedules extraction from successful turns, not from compaction.
|
|
@@ -295,7 +348,7 @@ async function runGate(ctx, engine, agent, config, state, snapshot, record, sign
|
|
|
295
348
|
}
|
|
296
349
|
state.running = true;
|
|
297
350
|
try {
|
|
298
|
-
await runMemoryExtractionPhase(ctx, engine, agent, config, state, snapshot, signal);
|
|
351
|
+
await runMemoryExtractionPhase(ctx, engine, agent, config, state, snapshot, record, signal);
|
|
299
352
|
await runReviewPhase(ctx, engine, agent, config, state, snapshot, record, signal);
|
|
300
353
|
// D3: a goal stuck in "blocked" for consecutive gate runs gets one
|
|
301
354
|
// local-fate assessment (the pipeline below), so whatever led the
|
|
@@ -343,14 +396,27 @@ export async function runGoalBlockedFate(ctx, engine, agent, config, state, _rea
|
|
|
343
396
|
* successful/no-op memory outcome; later review/fate failures do not replay
|
|
344
397
|
* old memory decisions, while memory failures leave the boundary retryable.
|
|
345
398
|
*/
|
|
346
|
-
export async function runMemoryExtractionPhase(ctx, engine, agent, config, state, snapshot, signal) {
|
|
399
|
+
export async function runMemoryExtractionPhase(ctx, engine, agent, config, state, snapshot, record, signal) {
|
|
400
|
+
const sessionId = agent.id;
|
|
401
|
+
const turnsSinceLastReview = state.turns - state.lastReviewAt;
|
|
402
|
+
const startedAt = Date.now();
|
|
347
403
|
const memorySnapshot = state.memoryCheckpoint ? sliceTurnSnapshot(snapshot, state.memoryCheckpoint) : snapshot;
|
|
348
404
|
if (!memorySnapshot.eligible || !memorySnapshot.trajectory) {
|
|
349
405
|
advanceMemoryCheckpoint(state, snapshot.cursor);
|
|
350
406
|
ctx.logger("continual-evolve").info(`memory agent no-op (${snapshot.reason}) [${agent.id}]: no new eligible evidence after checkpoint`);
|
|
407
|
+
record({
|
|
408
|
+
sessionId,
|
|
409
|
+
reason: snapshot.reason,
|
|
410
|
+
turnsSinceLastReview,
|
|
411
|
+
outcome: "noop",
|
|
412
|
+
rationale: `memory no-op (${snapshot.reason}): no new eligible evidence after checkpoint`,
|
|
413
|
+
durationMs: Date.now() - startedAt,
|
|
414
|
+
memoryTurns: 0,
|
|
415
|
+
memorySearches: 0,
|
|
416
|
+
appliedEdits: 0,
|
|
417
|
+
});
|
|
351
418
|
return;
|
|
352
419
|
}
|
|
353
|
-
const sessionId = agent.id;
|
|
354
420
|
const projectKey = snapshot.projectKey ?? projectKeyOf(agent);
|
|
355
421
|
const baselines = {
|
|
356
422
|
local: engine.load("local", sessionId),
|
|
@@ -393,6 +459,17 @@ export async function runMemoryExtractionPhase(ctx, engine, agent, config, state
|
|
|
393
459
|
if (run.proposal.edits.length === 0) {
|
|
394
460
|
advanceMemoryCheckpoint(state, memorySnapshot.cursor);
|
|
395
461
|
ctx.logger("continual-evolve").info(`memory agent no-op (${snapshot.reason}) [${sessionId}] after ${run.turns} turn(s): ${run.proposal.rationale}`);
|
|
462
|
+
record({
|
|
463
|
+
sessionId,
|
|
464
|
+
reason: snapshot.reason,
|
|
465
|
+
turnsSinceLastReview,
|
|
466
|
+
outcome: "noop",
|
|
467
|
+
rationale: `memory no-op (${snapshot.reason}) after ${run.turns} turn(s): ${run.proposal.rationale}`,
|
|
468
|
+
durationMs: Date.now() - startedAt,
|
|
469
|
+
memoryTurns: run.turns,
|
|
470
|
+
memorySearches: run.searches,
|
|
471
|
+
appliedEdits: 0,
|
|
472
|
+
});
|
|
396
473
|
return;
|
|
397
474
|
}
|
|
398
475
|
const source = { sessionId, ...(memorySnapshot.sourceSeqs.length > 0 ? { seqs: [...memorySnapshot.sourceSeqs] } : {}) };
|
|
@@ -413,7 +490,48 @@ export async function runMemoryExtractionPhase(ctx, engine, agent, config, state
|
|
|
413
490
|
state.memoryCheckpoint = memorySnapshot.cursor;
|
|
414
491
|
const applied = application.results.reduce((count, result) => count + result.appliedEdits.filter((edit) => edit.applied).length, 0);
|
|
415
492
|
const declined = application.declinedScopes.length > 0 ? `; declined scopes=${application.declinedScopes.join(",")}` : "";
|
|
493
|
+
const durationMs = Date.now() - startedAt;
|
|
494
|
+
for (const result of application.results) {
|
|
495
|
+
record({
|
|
496
|
+
sessionId,
|
|
497
|
+
reason: snapshot.reason,
|
|
498
|
+
turnsSinceLastReview,
|
|
499
|
+
outcome: "applied",
|
|
500
|
+
rationale: `memory applied (${snapshot.reason}) after ${run.turns} turn(s): ${run.proposal.rationale}${declined}`,
|
|
501
|
+
refinementId: result.id,
|
|
502
|
+
durationMs,
|
|
503
|
+
memoryTurns: run.turns,
|
|
504
|
+
memorySearches: run.searches,
|
|
505
|
+
appliedEdits: result.appliedEdits.filter((edit) => edit.applied).length,
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
if (application.declinedScopes.length > 0) {
|
|
509
|
+
record({
|
|
510
|
+
sessionId,
|
|
511
|
+
reason: snapshot.reason,
|
|
512
|
+
turnsSinceLastReview,
|
|
513
|
+
outcome: "declined",
|
|
514
|
+
rationale: `memory scopes declined (${snapshot.reason}): ${application.declinedScopes.join(",")}`,
|
|
515
|
+
durationMs,
|
|
516
|
+
memoryTurns: run.turns,
|
|
517
|
+
memorySearches: run.searches,
|
|
518
|
+
appliedEdits: 0,
|
|
519
|
+
});
|
|
520
|
+
}
|
|
416
521
|
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}`);
|
|
522
|
+
// Visibility (P1 unified receipt): only applied outcomes wake the agent
|
|
523
|
+
// with a follow-up — and only on the turn path, never mid-compaction.
|
|
524
|
+
// No-op and declined-only runs stay audit-only (reviews.jsonl).
|
|
525
|
+
if (config.notifyOnAutoReview && snapshot.reason === "turn_snapshot" && applied > 0) {
|
|
526
|
+
notifyMemoryExtraction(ctx, agent, {
|
|
527
|
+
outcome: "applied",
|
|
528
|
+
results: application.results,
|
|
529
|
+
...(application.declinedScopes.length > 0 ? { declinedScopes: [...application.declinedScopes] } : {}),
|
|
530
|
+
turns: run.turns,
|
|
531
|
+
searches: run.searches,
|
|
532
|
+
durationMs,
|
|
533
|
+
});
|
|
534
|
+
}
|
|
417
535
|
}
|
|
418
536
|
async function runReviewPhase(ctx, engine, agent, config, state, snapshot, record, signal) {
|
|
419
537
|
const sessionId = agent.id;
|
package/lib/benchmark-command.js
CHANGED
|
@@ -106,7 +106,7 @@ export async function executeBenchmarkCommand(ctx, engine, invocation, rest, run
|
|
|
106
106
|
for (const d of board.decisions) {
|
|
107
107
|
lines.push(`decision: ${d.accepted ? "ACCEPTED" : "rejected"} ${d.candidateLabel} — ${d.reasons.join("; ") || "ok"}`);
|
|
108
108
|
}
|
|
109
|
-
return success(lines.join("\n")
|
|
109
|
+
return success(lines.join("\n"));
|
|
110
110
|
}
|
|
111
111
|
case "run": {
|
|
112
112
|
const bid = stripAngleBrackets(args[0] ?? "");
|
package/lib/command.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { ARCHIVED_AT_KEY } from "./types.js";
|
|
1
|
+
import { ARCHIVED_AT_KEY, MEMORY_TYPE_KEY } from "./types.js";
|
|
2
2
|
import { formatHarnessStateForPrompt, historyForPrompt } from "./render.js";
|
|
3
|
+
import { compactText } from "./render.js";
|
|
3
4
|
import { planWithLlm } from "./planner.js";
|
|
4
5
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
6
|
import { join } from "node:path";
|
|
@@ -18,6 +19,7 @@ import { projectKeyOf } from "./project.js";
|
|
|
18
19
|
import { loadUsage, getUsageCount } from "./usage.js";
|
|
19
20
|
import { loadGateRuntime, saveGateRuntime } from "./runtime.js";
|
|
20
21
|
import { planConsolidation } from "./consolidate.js";
|
|
22
|
+
import { formatRecallResult, recallMemories, RECALL_MEMORY_TYPES } from "./recall.js";
|
|
21
23
|
import { loadTokenUsage, renderTokenUsageReport } from "./token-usage.js";
|
|
22
24
|
const USAGE = `Usage:
|
|
23
25
|
/evolve show this help and the current local store
|
|
@@ -30,6 +32,12 @@ const USAGE = `Usage:
|
|
|
30
32
|
/evolve archive <id> [global] hide an entry from injection (data kept, restorable)
|
|
31
33
|
/evolve unarchive <id> [global] restore an archived entry
|
|
32
34
|
/evolve demote <id> hide a (global) entry from injection, keep data
|
|
35
|
+
/evolve recall <query...> [scope] targeted memory recall: full content with version,
|
|
36
|
+
source, and staleness (same engine as the evolve_recall tool)
|
|
37
|
+
/evolve remember <type> [scope] <text...>
|
|
38
|
+
immediately persist one memory (type = user|feedback|project|reference;
|
|
39
|
+
feedback/project text must carry Why + How; default scope local)
|
|
40
|
+
/evolve forget [scope] <query...> locate one memory by query and archive it (restorable via /evolve unarchive)
|
|
33
41
|
/evolve consolidate [apply] [merge]
|
|
34
42
|
report (or apply) a batch archive of conflict-hinted
|
|
35
43
|
and stale zero-use global entries; "merge" folds
|
|
@@ -206,6 +214,90 @@ async function executeEvolveCommand(ctx, engine, invocation, opts, runtime) {
|
|
|
206
214
|
}, { scope });
|
|
207
215
|
return success(renderResult(result));
|
|
208
216
|
}
|
|
217
|
+
case "recall": {
|
|
218
|
+
// Human counterpart of the evolve_recall tool: same engine,
|
|
219
|
+
// same filters (memory kind, one scope or all three).
|
|
220
|
+
const { scope, rest: after } = scopeArg(rest);
|
|
221
|
+
const explicitScope = rest.length > 0 && (rest[0] === "global" || rest[0] === "project" || rest[0] === "local");
|
|
222
|
+
const query = after.join(" ");
|
|
223
|
+
const result = recallMemories(engine, { sessionId, projectKey: projectKeyOf(invocation.agent) ?? undefined }, { kinds: ["memory"], ...(explicitScope ? { scopes: [scope] } : {}), ...(query ? { query } : {}), limit: 10 });
|
|
224
|
+
return success(formatRecallResult(result, query || undefined));
|
|
225
|
+
}
|
|
226
|
+
case "remember": {
|
|
227
|
+
// Immediate manual memory path (P1 §6.6): one fact, explicit
|
|
228
|
+
// type, straight into the engine — validation, snapshot,
|
|
229
|
+
// versioning, and approval behave exactly like any other
|
|
230
|
+
// write. feedback/project text must already carry Why + How;
|
|
231
|
+
// the engine rejects it loudly otherwise.
|
|
232
|
+
const type = rest[0] ?? "";
|
|
233
|
+
if (!RECALL_MEMORY_TYPES.includes(type)) {
|
|
234
|
+
return error(`remember requires a memory type: user|feedback|project|reference.\nExample: /evolve remember feedback local 对方偏好深色主题。Why:…… How to apply:……\n${USAGE}`);
|
|
235
|
+
}
|
|
236
|
+
const maybeScope = rest[1] ?? "";
|
|
237
|
+
const scoped = maybeScope === "global" || maybeScope === "project" || maybeScope === "local";
|
|
238
|
+
const scope = scoped ? maybeScope : "local";
|
|
239
|
+
const text = (scoped ? rest.slice(2) : rest.slice(1)).join(" ").trim();
|
|
240
|
+
if (!text) {
|
|
241
|
+
return error(`remember requires the memory text after the type.\n${USAGE}`);
|
|
242
|
+
}
|
|
243
|
+
const storeId = storeIdForCommand(scope, invocation);
|
|
244
|
+
if ((scope === "global" || scope === "project") && opts.requireGlobalApproval) {
|
|
245
|
+
await requireGlobalApproval(ctx, invocation.agent, invocation.signal, `/evolve remember ${type} → ${scope} store: ${compactText(text, 120)}`);
|
|
246
|
+
}
|
|
247
|
+
const result = engine.apply(scope, storeId, {
|
|
248
|
+
summary: `remember: ${compactText(text, 80)}`,
|
|
249
|
+
rationale: "Human-invoked immediate memory via /evolve remember.",
|
|
250
|
+
expectedOutcome: "The fact is persisted as a typed memory entry.",
|
|
251
|
+
edits: [{
|
|
252
|
+
action: "create",
|
|
253
|
+
kind: "memory",
|
|
254
|
+
title: compactText(text, 80),
|
|
255
|
+
content: text,
|
|
256
|
+
metadata: { [MEMORY_TYPE_KEY]: type },
|
|
257
|
+
}],
|
|
258
|
+
}, { scope });
|
|
259
|
+
return success(renderResult(result));
|
|
260
|
+
}
|
|
261
|
+
case "forget": {
|
|
262
|
+
// Manual forget path (P1 §6.6): locate by query, archive the
|
|
263
|
+
// single match (restorable via unarchive). An ambiguous query
|
|
264
|
+
// lists candidates instead of archiving — a silent multi-
|
|
265
|
+
// archive would look like success while hiding too much.
|
|
266
|
+
const { scope, rest: after } = scopeArg(rest);
|
|
267
|
+
const explicitScope = rest.length > 0 && (rest[0] === "global" || rest[0] === "project" || rest[0] === "local");
|
|
268
|
+
const query = after.join(" ").trim();
|
|
269
|
+
if (!query) {
|
|
270
|
+
return error(`forget requires a query to locate the memory.\n${USAGE}`);
|
|
271
|
+
}
|
|
272
|
+
const recalled = recallMemories(engine, { sessionId, projectKey: projectKeyOf(invocation.agent) ?? undefined }, { query, kinds: ["memory"], ...(explicitScope ? { scopes: [scope] } : {}), limit: 6, includeArchived: false });
|
|
273
|
+
if (recalled.hits.length === 0) {
|
|
274
|
+
return error(`no memory matches "${query}"${recalled.notes.length > 0 ? ` (${recalled.notes.join("; ")})` : ""} — try fewer words, or /evolve list to browse.`);
|
|
275
|
+
}
|
|
276
|
+
if (recalled.hits.length > 1) {
|
|
277
|
+
const list = recalled.hits.map((hit) => `- [${hit.scope}:${hit.id}] ${hit.title}`).join("\n");
|
|
278
|
+
return success(`"${query}" matches ${recalled.hits.length} memories — refine the query so exactly one matches, then forget archives it:\n${list}`);
|
|
279
|
+
}
|
|
280
|
+
const hit = recalled.hits[0];
|
|
281
|
+
const state = engine.load(hit.scope, hit.scope === "local" ? sessionId : hit.scope === "project" ? (projectKeyOf(invocation.agent) ?? undefined) : undefined);
|
|
282
|
+
const entry = state.entries.memory[hit.id];
|
|
283
|
+
if (!entry) {
|
|
284
|
+
return error(`memory ${hit.scope}:${hit.id} vanished since recall — run /evolve forget again.`);
|
|
285
|
+
}
|
|
286
|
+
const result = engine.apply(hit.scope, hit.scope === "local" ? sessionId : hit.scope === "project" ? (projectKeyOf(invocation.agent) ?? undefined) : undefined, {
|
|
287
|
+
summary: `forget: archive memory ${hit.scope}:${hit.id}`,
|
|
288
|
+
rationale: `Human-invoked forget via /evolve forget "${query}".`,
|
|
289
|
+
expectedOutcome: "The entry is hidden from injection (data kept; restorable via /evolve unarchive).",
|
|
290
|
+
edits: [{
|
|
291
|
+
action: "update",
|
|
292
|
+
kind: "memory",
|
|
293
|
+
id: hit.id,
|
|
294
|
+
title: entry.title,
|
|
295
|
+
content: entry.content,
|
|
296
|
+
metadata: { ...entry.metadata, [ARCHIVED_AT_KEY]: new Date().toISOString() },
|
|
297
|
+
}],
|
|
298
|
+
}, { scope: hit.scope });
|
|
299
|
+
return success(`forgot [${hit.scope}:${hit.id}] ${hit.title} (archived — restore with /evolve unarchive ${hit.id}${hit.scope === "local" ? "" : ` ${hit.scope}`})\n${renderResult(result)}`);
|
|
300
|
+
}
|
|
209
301
|
case "consolidate": {
|
|
210
302
|
// R3: deterministic global-store hygiene. Report by default;
|
|
211
303
|
// `apply` re-scans fresh state and lands the whole batch as ONE
|
package/lib/fate.js
CHANGED
|
@@ -241,7 +241,10 @@ export async function runLocalFatePhase(ctx, engine, agent, config, state, reaso
|
|
|
241
241
|
const plan = planLocalFates(assessment.items, candidates, globalState, config.promotionPolicy);
|
|
242
242
|
const needsDialog = plan.promotable.length + plan.splits.length + plan.reviewArchives.length > 0;
|
|
243
243
|
let consent = { approved: false, asked: false, reason: "nothing-to-ask" };
|
|
244
|
-
|
|
244
|
+
// Turn-path tightening (2026-09-25): turn_snapshot/turn_interval never open
|
|
245
|
+
// a dialog — governed actions are deferred to `/evolve wrapup`. Only an
|
|
246
|
+
// explicit goal_blocked signal (own streak counter) may consult.
|
|
247
|
+
if (reason === "goal_blocked" && needsDialog) {
|
|
245
248
|
consent = await consultLocalFates(ctx, agent, plan, state);
|
|
246
249
|
}
|
|
247
250
|
if (consent.approved) {
|
|
@@ -255,9 +258,6 @@ export async function runLocalFatePhase(ctx, engine, agent, config, state, reaso
|
|
|
255
258
|
rationale: `fate: ${assessment.rationale} (${applied.join("; ")})`,
|
|
256
259
|
refinementId: results.map((result) => result.id).join(","),
|
|
257
260
|
});
|
|
258
|
-
if (config.notifyOnAutoReview && (reason === "turn_snapshot" || reason === "turn_interval") && applied.length > 0) {
|
|
259
|
-
notifyFateApplied(ctx, agent, applied);
|
|
260
|
-
}
|
|
261
261
|
return;
|
|
262
262
|
}
|
|
263
263
|
if (consent.reason === "declined" || consent.reason === "unavailable" || consent.reason === "error") {
|
|
@@ -273,18 +273,18 @@ export async function runLocalFatePhase(ctx, engine, agent, config, state, reaso
|
|
|
273
273
|
});
|
|
274
274
|
return;
|
|
275
275
|
}
|
|
276
|
-
if (reason
|
|
277
|
-
//
|
|
278
|
-
// governed actions are deferred with an audit record.
|
|
276
|
+
if (needsDialog && reason !== "goal_blocked") {
|
|
277
|
+
// Turn/compact path: no dialog. Only deterministic silent archives apply;
|
|
278
|
+
// governed actions are deferred with an audit record pointing at wrapup.
|
|
279
279
|
const { applied, results } = applyLocalFates(engine, sessionId, plan, localState, "silent-only");
|
|
280
280
|
if (applied.length > 0) {
|
|
281
|
-
logger.info(`auto-review local-fate (
|
|
281
|
+
logger.info(`auto-review local-fate (${reason}) [${sessionId}]: silent-archived ${applied.length} — ${assessment.rationale}`);
|
|
282
282
|
record({
|
|
283
283
|
sessionId,
|
|
284
284
|
reason,
|
|
285
285
|
turnsSinceLastReview: turnsSinceFate,
|
|
286
286
|
outcome: "approved",
|
|
287
|
-
rationale: `fate (
|
|
287
|
+
rationale: `fate (${reason}): ${assessment.rationale} (${applied.join("; ")})`,
|
|
288
288
|
refinementId: results.map((result) => result.id).join(","),
|
|
289
289
|
});
|
|
290
290
|
}
|
|
@@ -293,7 +293,7 @@ export async function runLocalFatePhase(ctx, engine, agent, config, state, reaso
|
|
|
293
293
|
reason,
|
|
294
294
|
turnsSinceLastReview: turnsSinceFate,
|
|
295
295
|
outcome: "deferred",
|
|
296
|
-
rationale: `fate (
|
|
296
|
+
rationale: `fate (${reason}): ${plan.promotable.length} promotes, ${plan.splits.length} splits, ${plan.reviewArchives.length} review-archives deferred — run /evolve wrapup for a full session exit`,
|
|
297
297
|
});
|
|
298
298
|
return;
|
|
299
299
|
}
|