dsh-continual-evolve 0.9.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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-1031%20passing-brightgreen)]()
10
+ [![Tests](https://img.shields.io/badge/tests-1051%20passing-brightgreen)]()
11
11
  [![Coverage · statements](https://img.shields.io/badge/coverage_statements-98%25-brightgreen)]()
12
12
  [![Coverage · branches](https://img.shields.io/badge/coverage_branches-92%25-green)]()
13
13
  [![Coverage · functions](https://img.shields.io/badge/coverage_functions-99%25-brightgreen)]()
@@ -142,7 +142,7 @@ before aborting.
142
142
 
143
143
  ```bash
144
144
  pnpm install && pnpm build # deps + tsc -> lib/
145
- pnpm test # vitest (1031 tests)
145
+ pnpm test # vitest (1051 tests)
146
146
  pnpm test:coverage # v8 coverage, thresholds enforced in CI
147
147
  pnpm coverage:gaps # locate uncovered lines per file (read-only)
148
148
  pnpm lint # oxlint src test
@@ -152,7 +152,7 @@ Project layout:
152
152
 
153
153
  ```
154
154
  ├── src/ # engine, tools, commands, memory agent, recall, projection, gate, fate, benchmark, injection + token usage…
155
- ├── test/ # vitest suites (50 files)
155
+ ├── test/ # vitest suites (56 files)
156
156
  ├── lib/ # build output (tsc)
157
157
  ├── docs/
158
158
  │ ├── 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-1031%20passing-brightgreen)]()
10
+ [![Tests](https://img.shields.io/badge/tests-1051%20passing-brightgreen)]()
11
11
  [![Coverage · statements](https://img.shields.io/badge/coverage_statements-98%25-brightgreen)]()
12
12
  [![Coverage · branches](https://img.shields.io/badge/coverage_branches-92%25-green)]()
13
13
  [![Coverage · functions](https://img.shields.io/badge/coverage_functions-99%25-brightgreen)]()
@@ -126,7 +126,7 @@ profile patch 示例:
126
126
 
127
127
  ```bash
128
128
  pnpm install && pnpm build # 依赖 + tsc -> lib/
129
- pnpm test # vitest(1031 例)
129
+ pnpm test # vitest(1051 例)
130
130
  pnpm test:coverage # v8 覆盖率,CI 强制阈值
131
131
  pnpm coverage:gaps # 定位各文件未覆盖行(只读)
132
132
  pnpm lint # oxlint src test
@@ -136,7 +136,7 @@ pnpm lint # oxlint src test
136
136
 
137
137
  ```
138
138
  ├── src/ # 引擎、工具、命令、memory Agent、召回、投影、门禁、fate、benchmark、注入 + token 用量…
139
- ├── test/ # vitest 测试套件(50 个文件)
139
+ ├── test/ # vitest 测试套件(56 个文件)
140
140
  ├── lib/ # 构建产物(tsc)
141
141
  ├── docs/
142
142
  │ ├── design.md # 完整设计文档(硬化矩阵)
package/lib/approval.d.ts CHANGED
@@ -36,6 +36,20 @@ export declare function questionServiceOf(ctx: Context): QuestionService | undef
36
36
  * Ask once for a persistent-scope edit. Only a unique, explicit approval
37
37
  * choice is accepted. A missing/ambiguous/unknown answer throws so callers
38
38
  * never interpret a lost or malformed dialog as a durable rejection.
39
+ *
40
+ * Copy follows the headline → details → impact shape: the first line names
41
+ * the target store, the middle carries the (truncated) edit summary, and the
42
+ * last line states visibility/reversibility. Option labels stay literal
43
+ * ("批准"/"拒绝") for parser compatibility; human guidance lives in
44
+ * `description`.
45
+ *
46
+ * @param ctx Cordis context carrying the userQuestions service.
47
+ * @param agent Agent prompting the question, if any.
48
+ * @param signal Abort signal for the question, if any.
49
+ * @param scope Target persistent scope.
50
+ * @param what Edit summary; truncated to 300 chars with "…" when longer.
51
+ * @returns "approved" when the user picks "批准", otherwise "declined".
52
+ * @throws When the service is missing or the answer is not unique/explicit.
39
53
  */
40
54
  export declare function requestScopeApproval(ctx: Context, agent: Agent | undefined, signal: AbortSignal | undefined, scope: "project" | "global", what: string): Promise<ScopeApprovalDecision>;
41
55
  /**
package/lib/approval.js CHANGED
@@ -10,6 +10,20 @@ export function questionServiceOf(ctx) {
10
10
  * Ask once for a persistent-scope edit. Only a unique, explicit approval
11
11
  * choice is accepted. A missing/ambiguous/unknown answer throws so callers
12
12
  * never interpret a lost or malformed dialog as a durable rejection.
13
+ *
14
+ * Copy follows the headline → details → impact shape: the first line names
15
+ * the target store, the middle carries the (truncated) edit summary, and the
16
+ * last line states visibility/reversibility. Option labels stay literal
17
+ * ("批准"/"拒绝") for parser compatibility; human guidance lives in
18
+ * `description`.
19
+ *
20
+ * @param ctx Cordis context carrying the userQuestions service.
21
+ * @param agent Agent prompting the question, if any.
22
+ * @param signal Abort signal for the question, if any.
23
+ * @param scope Target persistent scope.
24
+ * @param what Edit summary; truncated to 300 chars with "…" when longer.
25
+ * @returns "approved" when the user picks "批准", otherwise "declined".
26
+ * @throws When the service is missing or the answer is not unique/explicit.
13
27
  */
14
28
  export async function requestScopeApproval(ctx, agent, signal, scope, what) {
15
29
  const userQuestions = questionServiceOf(ctx);
@@ -17,12 +31,20 @@ export async function requestScopeApproval(ctx, agent, signal, scope, what) {
17
31
  throw new Error("global evolution edits require the userQuestions service (load @deepseek-ai/dsh-user-questions)");
18
32
  }
19
33
  const storeLabel = scope === "project" ? "本项目跨会话 store" : "跨会话全局 store";
34
+ const compactWhat = what.length > 300 ? `${what.slice(0, 300)}…` : what;
35
+ const impact = scope === "project" ? "影响:仅本项目会话可见,可回滚。" : "影响:所有会话可见,可回滚。";
20
36
  const answer = await userQuestions.ask({
21
37
  questions: [
22
38
  {
23
39
  id: "approve-global-evolve",
24
- question: `批准写入${storeLabel}?\n\n${what}`,
25
- options: [{ label: "批准" }, { label: "拒绝" }],
40
+ question: `写入${storeLabel}?\n${compactWhat}\n${impact}`,
41
+ options: [
42
+ {
43
+ label: "批准",
44
+ description: scope === "project" ? "写入,仅本项目会话可见" : "写入,所有会话可见",
45
+ },
46
+ { label: "拒绝", description: "不写入,本次跳过" },
47
+ ],
26
48
  },
27
49
  ],
28
50
  ...(agent ? { agent } : {}),
package/lib/auto.js CHANGED
@@ -34,6 +34,8 @@ import { isGateEnabled } from "./runtime.js";
34
34
  import { compareReviewCursors, createReviewScheduler } from "./review-scheduler.js";
35
35
  import { captureTurnSnapshot, sliceTurnSnapshot } from "./turn-snapshot.js";
36
36
  import { captureAutoCase } from "./autocase.js";
37
+ import { loadDeclinedMemory, matchDeclinedCheckpoint } from "./declines.js";
38
+ import { tokenize } from "./search.js";
37
39
  import { applyMemoryExtractionProposal, buildMemoryManifest, runMemoryAgent, } from "./memory-agent.js";
38
40
  import { asLlmSessionId } from "./llm-text.js";
39
41
  /** Turns a rejected skill candidate stays silent before being offered again. */
@@ -417,6 +419,23 @@ export async function runMemoryExtractionPhase(ctx, engine, agent, config, state
417
419
  });
418
420
  return;
419
421
  }
422
+ const declinedHit = matchDeclinedCheckpoint(loadDeclinedMemory(engine.baseDir), tokenize(memorySnapshot.trajectory));
423
+ if (declinedHit !== undefined) {
424
+ advanceMemoryCheckpoint(state, memorySnapshot.cursor);
425
+ ctx.logger("continual-evolve").info(`memory agent pre-suppressed (${snapshot.reason}) [${agent.id}]: checkpoint overlaps declined ${declinedHit.entry.scope}:${declinedHit.entry.fingerprint} (coverage ${(declinedHit.coverage * 100).toFixed(0)}%, ${declinedHit.hits} tokens) — agent not invoked`);
426
+ record({
427
+ sessionId,
428
+ reason: snapshot.reason,
429
+ turnsSinceLastReview,
430
+ outcome: "noop",
431
+ rationale: `memory pre-suppressed (${snapshot.reason}): checkpoint overlaps declined ${declinedHit.entry.scope}:${declinedHit.entry.fingerprint} — agent not invoked`,
432
+ durationMs: Date.now() - startedAt,
433
+ memoryTurns: 0,
434
+ memorySearches: 0,
435
+ appliedEdits: 0,
436
+ });
437
+ return;
438
+ }
420
439
  const projectKey = snapshot.projectKey ?? projectKeyOf(agent);
421
440
  const baselines = {
422
441
  local: engine.load("local", sessionId),
@@ -725,8 +744,11 @@ export async function consultSkillEdits(ctx, agent, skillEdits, gate) {
725
744
  questions: [
726
745
  {
727
746
  id: "evolve-skill-consult",
728
- question: `自进化检测到反复出现的流程/技能候选,建议沉淀:\n\n${description}\n\n是否固化?`,
729
- options: [{ label: "固化" }, { label: "不固化" }],
747
+ question: `发现可复用的流程,建议固化为技能\n${description}\n\n固化后以后同类任务自动复用,可回滚。是否固化?`,
748
+ options: [
749
+ { label: "固化", description: "生成技能,以后复用" },
750
+ { label: "不固化", description: "本次跳过,10 回合内不再打扰" },
751
+ ],
730
752
  },
731
753
  ],
732
754
  agent,
package/lib/command.js CHANGED
@@ -58,7 +58,7 @@ const USAGE = `Usage:
58
58
  export function registerEvolveCommand(ctx, engine, opts, runtime) {
59
59
  ctx.commands.register({
60
60
  name: "evolve",
61
- description: "inspect and evolve the continual harness state (memories, skills, prompt notes, subagent specs)",
61
+ description: "自进化:检查并演进 harness 状态(记忆/技能/提示词/子代理) | Inspect and evolve the continual harness state (memories, skills, prompt notes, subagent specs)",
62
62
  input: { hint: "[list [global] | history [global] | rollback <id> [global] | plan [msg]]" },
63
63
  handler: (invocation) => executeEvolveCommand(ctx, engine, invocation, opts, runtime),
64
64
  });
@@ -0,0 +1,101 @@
1
+ /** Cap on remembered declines; oldest-first pruning keeps the file small. */
2
+ export declare const MAX_DECLINED_MEMORY_LEDGER = 50;
3
+ /** Cap on stored content tokens per entry; enough for coverage, never prose. */
4
+ export declare const MAX_DECLINED_ENTRY_TOKENS = 128;
5
+ /** Minimum coverage of an entry's tokens by the checkpoint to pre-suppress. */
6
+ export declare const DECLINED_PRECHECK_COVERAGE = 0.85;
7
+ /** Minimum overlapping tokens; tiny entries must never match vacuously. */
8
+ export declare const DECLINED_PRECHECK_MIN_TOKENS = 5;
9
+ /** One declined project/global batch, keyed by its content fingerprint. */
10
+ export interface DeclinedMemoryBatch {
11
+ /** Persistent scope that was declined. Local never asks, so never lands here. */
12
+ scope: "project" | "global";
13
+ /** sha256-16 over the normalized batch (same cut as evaluate.ts/project.ts). */
14
+ fingerprint: string;
15
+ /** First edit title (audit readability only, never matched on). */
16
+ title: string;
17
+ /** Deduplicated content tokens for checkpoint containment checks. */
18
+ tokens: string[];
19
+ /** ISO timestamp of the human rejection. */
20
+ declinedAt: string;
21
+ }
22
+ /** Minimal edit shape needed for fingerprinting (avoids importing the agent). */
23
+ export interface FingerprintableEdit {
24
+ action: string;
25
+ id?: string;
26
+ title?: string;
27
+ content?: string;
28
+ }
29
+ /** Full path of the decline ledger file. */
30
+ export declare function declinedMemoryPath(baseDir: string): string;
31
+ /**
32
+ * Fingerprint one scope batch. Normalization is whitespace-only on purpose:
33
+ * case folding could conflate distinct facts (e.g. env var names), and a
34
+ * missed match only costs one more popup (fail-closed).
35
+ *
36
+ * @param scope - persistent scope the batch targets.
37
+ * @param edits - the scope batch in proposal order (order is significant).
38
+ * @returns 16-char hex fingerprint.
39
+ */
40
+ export declare function fingerprintMemoryBatch(scope: "project" | "global", edits: readonly FingerprintableEdit[]): string;
41
+ /**
42
+ * Load the decline ledger. Missing or malformed files read as empty so a
43
+ * broken ledger degrades to the status quo (ask anyway), never to a crash
44
+ * on the approval path.
45
+ *
46
+ * @param baseDir - engine base directory.
47
+ * @returns validated ledger entries (possibly empty).
48
+ */
49
+ export declare function loadDeclinedMemory(baseDir: string): DeclinedMemoryBatch[];
50
+ /**
51
+ * Whether this exact batch was declined before.
52
+ *
53
+ * @param entries - loaded ledger.
54
+ * @param scope - persistent scope of the pending batch.
55
+ * @param fingerprint - fingerprint of the pending batch.
56
+ */
57
+ export declare function isDeclinedRepeat(entries: readonly DeclinedMemoryBatch[], scope: "project" | "global", fingerprint: string): boolean;
58
+ /**
59
+ * Record one human decline, pruning oldest-first past the cap. The batch
60
+ * edits supply fingerprint material, match tokens, and the secret screen:
61
+ * a secret-shaped decline is still declined, but never lands in the ledger
62
+ * (it was never engine-screened, so it must not be persisted anywhere).
63
+ *
64
+ * @param baseDir - engine base directory.
65
+ * @param scope - persistent scope that was declined.
66
+ * @param edits - the declined scope batch in proposal order.
67
+ * @param title - first edit title, kept for audit readability.
68
+ * @returns the ledger file path, or undefined when skipped by the secret screen.
69
+ * @throws On filesystem write failure; callers on the approval path should
70
+ * contain it (suppression is an optimization, never load-bearing).
71
+ */
72
+ export declare function recordDeclinedMemory(baseDir: string, scope: "project" | "global", edits: readonly FingerprintableEdit[], title: string): string | undefined;
73
+ /**
74
+ * Deduplicated content tokens of one batch, capped. Shared CJK-aware
75
+ * tokenizer with the manifest ranking, so both sides of the containment
76
+ * check speak the same token language.
77
+ *
78
+ * @param edits - batch edits.
79
+ * @returns at most MAX_DECLINED_ENTRY_TOKENS distinct tokens.
80
+ */
81
+ export declare function declinedContentTokens(edits: readonly FingerprintableEdit[]): string[];
82
+ /** One checkpoint containment hit: which entry matched and how strongly. */
83
+ export interface DeclinedCheckpointHit {
84
+ /** Ledger entry whose content the checkpoint re-covers. */
85
+ entry: DeclinedMemoryBatch;
86
+ /** Fraction of the entry's tokens present in the checkpoint. */
87
+ coverage: number;
88
+ /** Overlapping token count. */
89
+ hits: number;
90
+ }
91
+ /**
92
+ * Match a pending checkpoint against declined content (entry ⊆ checkpoint).
93
+ * Conservative by design: high coverage plus an absolute token floor, so
94
+ * tiny or vague declines can never suppress anything.
95
+ *
96
+ * @param entries - loaded ledger.
97
+ * @param checkpointTokens - tokens of the pending checkpoint trajectory.
98
+ * @returns the strongest hit, or undefined when nothing clears the floor.
99
+ */
100
+ export declare function matchDeclinedCheckpoint(entries: readonly DeclinedMemoryBatch[], checkpointTokens: readonly string[]): DeclinedCheckpointHit | undefined;
101
+ //# sourceMappingURL=declines.d.ts.map
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Cross-session ledger of declined memory proposals (the decline loop).
3
+ *
4
+ * In-memory `GateState.memoryDecisions` dies on restart and its decision
5
+ * cursor is per-checkpoint, so a declined project/global batch is re-asked
6
+ * in the next session. This file persists batch fingerprints plus content
7
+ * tokens under `<baseDir>/evolve/declined-memory.json`: exact batches are
8
+ * suppressed at approval time, and checkpoints overlapping declined content
9
+ * are suppressed before the agent is even invoked. Token matching is
10
+ * containment (entry ⊆ checkpoint) with a conservative floor; rephrasing
11
+ * below the floor still asks (documented limitation, not silent loss).
12
+ */
13
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
14
+ import { createHash } from "node:crypto";
15
+ import { join } from "node:path";
16
+ import { secretLeakReason } from "./promotion.js";
17
+ import { tokenize } from "./search.js";
18
+ import { EVOLVE_DIR } from "./store.js";
19
+ /** Cap on remembered declines; oldest-first pruning keeps the file small. */
20
+ export const MAX_DECLINED_MEMORY_LEDGER = 50;
21
+ /** Cap on stored content tokens per entry; enough for coverage, never prose. */
22
+ export const MAX_DECLINED_ENTRY_TOKENS = 128;
23
+ /** Minimum coverage of an entry's tokens by the checkpoint to pre-suppress. */
24
+ export const DECLINED_PRECHECK_COVERAGE = 0.85;
25
+ /** Minimum overlapping tokens; tiny entries must never match vacuously. */
26
+ export const DECLINED_PRECHECK_MIN_TOKENS = 5;
27
+ const DECLINED_MEMORY_FILE = "declined-memory.json";
28
+ /** Full path of the decline ledger file. */
29
+ export function declinedMemoryPath(baseDir) {
30
+ return join(baseDir, EVOLVE_DIR, DECLINED_MEMORY_FILE);
31
+ }
32
+ /**
33
+ * Fingerprint one scope batch. Normalization is whitespace-only on purpose:
34
+ * case folding could conflate distinct facts (e.g. env var names), and a
35
+ * missed match only costs one more popup (fail-closed).
36
+ *
37
+ * @param scope - persistent scope the batch targets.
38
+ * @param edits - the scope batch in proposal order (order is significant).
39
+ * @returns 16-char hex fingerprint.
40
+ */
41
+ export function fingerprintMemoryBatch(scope, edits) {
42
+ const parts = [scope];
43
+ for (const edit of edits) {
44
+ parts.push([edit.action, edit.id ?? "", edit.title ?? "", edit.content ?? ""].join("|"));
45
+ }
46
+ const material = parts.join("\n").replace(/\s+/g, " ").trim();
47
+ return createHash("sha256").update(material, "utf8").digest("hex").slice(0, 16);
48
+ }
49
+ /**
50
+ * Load the decline ledger. Missing or malformed files read as empty so a
51
+ * broken ledger degrades to the status quo (ask anyway), never to a crash
52
+ * on the approval path.
53
+ *
54
+ * @param baseDir - engine base directory.
55
+ * @returns validated ledger entries (possibly empty).
56
+ */
57
+ export function loadDeclinedMemory(baseDir) {
58
+ const path = declinedMemoryPath(baseDir);
59
+ if (!existsSync(path))
60
+ return [];
61
+ try {
62
+ const raw = JSON.parse(readFileSync(path, "utf8"));
63
+ if (!Array.isArray(raw))
64
+ return [];
65
+ return raw
66
+ .filter((entry) => {
67
+ if (typeof entry !== "object" || entry === null)
68
+ return false;
69
+ const candidate = entry;
70
+ return ((candidate.scope === "project" || candidate.scope === "global") &&
71
+ typeof candidate.fingerprint === "string");
72
+ })
73
+ .map((entry) => ({
74
+ ...entry,
75
+ tokens: Array.isArray(entry.tokens) ? entry.tokens.filter((token) => typeof token === "string") : [],
76
+ }));
77
+ }
78
+ catch {
79
+ return [];
80
+ }
81
+ }
82
+ /**
83
+ * Whether this exact batch was declined before.
84
+ *
85
+ * @param entries - loaded ledger.
86
+ * @param scope - persistent scope of the pending batch.
87
+ * @param fingerprint - fingerprint of the pending batch.
88
+ */
89
+ export function isDeclinedRepeat(entries, scope, fingerprint) {
90
+ return entries.some((entry) => entry.scope === scope && entry.fingerprint === fingerprint);
91
+ }
92
+ /**
93
+ * Record one human decline, pruning oldest-first past the cap. The batch
94
+ * edits supply fingerprint material, match tokens, and the secret screen:
95
+ * a secret-shaped decline is still declined, but never lands in the ledger
96
+ * (it was never engine-screened, so it must not be persisted anywhere).
97
+ *
98
+ * @param baseDir - engine base directory.
99
+ * @param scope - persistent scope that was declined.
100
+ * @param edits - the declined scope batch in proposal order.
101
+ * @param title - first edit title, kept for audit readability.
102
+ * @returns the ledger file path, or undefined when skipped by the secret screen.
103
+ * @throws On filesystem write failure; callers on the approval path should
104
+ * contain it (suppression is an optimization, never load-bearing).
105
+ */
106
+ export function recordDeclinedMemory(baseDir, scope, edits, title) {
107
+ const screenable = edits.map((edit) => `${edit.title ?? ""}\n${edit.content ?? ""}`).join("\n");
108
+ if (secretLeakReason(screenable) !== undefined)
109
+ return undefined;
110
+ const entries = loadDeclinedMemory(baseDir);
111
+ entries.push({
112
+ scope,
113
+ fingerprint: fingerprintMemoryBatch(scope, edits),
114
+ title,
115
+ tokens: declinedContentTokens(edits),
116
+ declinedAt: new Date().toISOString(),
117
+ });
118
+ entries.sort((left, right) => (left.declinedAt < right.declinedAt ? -1 : 1));
119
+ while (entries.length > MAX_DECLINED_MEMORY_LEDGER)
120
+ entries.shift();
121
+ const path = declinedMemoryPath(baseDir);
122
+ mkdirSync(join(baseDir, EVOLVE_DIR), { recursive: true });
123
+ writeFileSync(path, `${JSON.stringify(entries, null, 2)}\n`, "utf8");
124
+ return path;
125
+ }
126
+ /**
127
+ * Deduplicated content tokens of one batch, capped. Shared CJK-aware
128
+ * tokenizer with the manifest ranking, so both sides of the containment
129
+ * check speak the same token language.
130
+ *
131
+ * @param edits - batch edits.
132
+ * @returns at most MAX_DECLINED_ENTRY_TOKENS distinct tokens.
133
+ */
134
+ export function declinedContentTokens(edits) {
135
+ const seen = new Set();
136
+ for (const edit of edits) {
137
+ for (const token of tokenize(`${edit.title ?? ""} ${edit.content ?? ""}`)) {
138
+ seen.add(token);
139
+ if (seen.size >= MAX_DECLINED_ENTRY_TOKENS)
140
+ return [...seen];
141
+ }
142
+ }
143
+ return [...seen];
144
+ }
145
+ /**
146
+ * Match a pending checkpoint against declined content (entry ⊆ checkpoint).
147
+ * Conservative by design: high coverage plus an absolute token floor, so
148
+ * tiny or vague declines can never suppress anything.
149
+ *
150
+ * @param entries - loaded ledger.
151
+ * @param checkpointTokens - tokens of the pending checkpoint trajectory.
152
+ * @returns the strongest hit, or undefined when nothing clears the floor.
153
+ */
154
+ export function matchDeclinedCheckpoint(entries, checkpointTokens) {
155
+ const pending = new Set(checkpointTokens);
156
+ let best;
157
+ for (const entry of entries) {
158
+ if (entry.tokens.length === 0)
159
+ continue;
160
+ let hits = 0;
161
+ for (const token of entry.tokens) {
162
+ if (pending.has(token))
163
+ hits += 1;
164
+ }
165
+ if (hits < DECLINED_PRECHECK_MIN_TOKENS)
166
+ continue;
167
+ const coverage = hits / entry.tokens.length;
168
+ if (coverage < DECLINED_PRECHECK_COVERAGE)
169
+ continue;
170
+ if (best === undefined || coverage > best.coverage)
171
+ best = { entry, coverage, hits };
172
+ }
173
+ return best;
174
+ }
175
+ //# sourceMappingURL=declines.js.map
package/lib/fate.js CHANGED
@@ -92,7 +92,10 @@ export async function consultLocalFates(ctx, agent, plan, gate) {
92
92
  {
93
93
  id: "evolve-fate-consult",
94
94
  question: consultQuestion(plan),
95
- options: [{ label: "执行" }, { label: "不执行" }],
95
+ options: [
96
+ { label: "执行", description: "提升写全局,归档隐藏本地(均可恢复)" },
97
+ { label: "不执行", description: "全部保留,10 回合内不再打扰" },
98
+ ],
96
99
  },
97
100
  ],
98
101
  agent,
@@ -112,7 +115,7 @@ function consultQuestion(plan) {
112
115
  const byKey = new Map(plan.candidates.map((candidate) => [candidateKey(candidate.kind, candidate.id), candidate]));
113
116
  const lines = [];
114
117
  if (plan.promotable.length > 0 || plan.splits.length > 0) {
115
- lines.push("【提升到跨会话全局 store】");
118
+ lines.push("【提升到全局(写入全局 store)】");
116
119
  for (const item of plan.promotable) {
117
120
  lines.push(`- ${item.key}「${byKey.get(item.key)?.title ?? item.key}」 — ${item.reason}`);
118
121
  }
@@ -121,15 +124,15 @@ function consultQuestion(plan) {
121
124
  }
122
125
  }
123
126
  if (plan.reviewArchives.length > 0) {
124
- lines.push("【归档(未被全局覆盖且源自真实对话,需确认)】");
127
+ lines.push("【归档(本地隐藏,可恢复)】");
125
128
  for (const item of plan.reviewArchives) {
126
129
  lines.push(`- ${item.key}「${byKey.get(item.key)?.title ?? item.key}」 — ${item.reason}`);
127
130
  }
128
131
  }
129
132
  return [
130
- "自进化门禁检测到本会话的 local 条目需要归宿处理(提升条目将写入跨会话全局 store,归档条目隐藏但可恢复):",
133
+ "自进化门禁:本会话 local 条目需要归宿处理",
131
134
  ...lines,
132
- "是否执行?",
135
+ "提升写入后所有会话可见,归档隐藏但可恢复。是否执行?",
133
136
  ].join("\n");
134
137
  }
135
138
  /**
@@ -61,7 +61,7 @@ export interface MemoryApplicationResult {
61
61
  /** Provider-facing schemas for the closed memory-only tool set. */
62
62
  export declare const MEMORY_AGENT_TOOL_SCHEMAS: readonly ToolSchema[];
63
63
  /** Instructions for the dedicated loop; model text is always untrusted. */
64
- export declare const MEMORY_AGENT_SYSTEM_PROMPT = "You are the dedicated background memory extraction agent for a continual-evolution harness.\n\nYou receive only an incremental conversation checkpoint and a frozen manifest of existing memory entries. Your entire job is to decide whether this checkpoint contains a durable fact worth remembering. Never propose prompt notes, skills, subagent specs, code changes, or general conversation summaries.\n\nRules:\n- Use memory_search before creating when the manifest may already cover the topic. Update an existing entry instead of creating a near-duplicate.\n- Every memory entry contains exactly one fact. Classify it with metadata.memoryType = user | feedback | project | reference.\n- feedback and project memories must state the fact, Why it matters, and How to apply it next time.\n- Never invent a user preference. Ground every proposal in the supplied conversation evidence and existing memory.\n- Every edit must have kind=memory, an explicit targetScope, and a coherent blastRadius.\n- targetScope=local is session staging. project persists within the current repository. global persists across projects. project/global require human approval before application.\n- update/delete/archive must name an existing id from the same targetScope. Prefer archive over delete when the fact is obsolete but should remain restorable.\n- Do not remember: code structure or file paths re-readable from the repository; git history, diffs, commit hashes, or CI logs; content already written in project instruction files; temporary session state, one-off debugging trails, or current task progress; model-only speculation without conversation evidence.\n- Be specific enough to act on: prefer a fact that tells the next session what to DO over a summary of what happened. If the fact is obvious from the conversation itself with no future action attached, skip it.\n- Save only what is surprising or non-obvious: routine chatter, restated context, and facts with no bearing on future work are no-ops, not memories.\n- Granularity examples (vague \u2192 reject; sharp \u2192 save):\n - \"user communicates in Chinese\" \u2192 \"user reads English with effort \u2014 write memory and handoff content in Chinese prose\".\n - \"user works on the desktop project\" \u2192 \"desktop profile loads npm-installed plugin copies \u2014 source edits need a release before they take effect\".\n- You cannot read or write source files, call agents, use MCP, use the network, or mutate the harness directly. memory_propose is the only finish tool and never writes state itself.\n- If no durable memory is justified, call memory_propose with edits=[].\n- Always finish by calling memory_propose. Do not return unproposed prose.";
64
+ export declare const MEMORY_AGENT_SYSTEM_PROMPT = "You are the dedicated background memory extraction agent for a continual-evolution harness.\n\nYou receive only an incremental conversation checkpoint and a frozen manifest of existing memory entries. Your entire job is to decide whether this checkpoint contains a durable fact worth remembering. Never propose prompt notes, skills, subagent specs, code changes, or general conversation summaries.\n\nRules:\n- Use memory_search before creating when the manifest may already cover the topic. Update an existing entry instead of creating a near-duplicate.\n- Every memory entry contains exactly one fact. Classify it with metadata.memoryType = user | feedback | project | reference.\n- feedback and project memories must state the fact, Why it matters, and How to apply it next time.\n- Never invent a user preference. Ground every proposal in the supplied conversation evidence and existing memory.\n- Every edit must have kind=memory, an explicit targetScope, and a coherent blastRadius.\n- targetScope=local is session staging. project persists within the current repository. global persists across projects. project/global require human approval before application.\n- update/delete/archive must name an existing id from the same targetScope. Prefer archive over delete when the fact is obsolete but should remain restorable.\n- Do not remember: code structure or file paths re-readable from the repository; git history, diffs, commit hashes, or CI logs; content already written in project instruction files; temporary session state, one-off debugging trails, or current task progress; model-only speculation without conversation evidence; an already-applied troubleshooting fix with no recurrence evidence (the fixed state on disk is its own record).\n- Be specific enough to act on: prefer a fact that tells the next session what to DO over a summary of what happened. If the fact is obvious from the conversation itself with no future action attached, skip it.\n- Save only what is surprising or non-obvious: routine chatter, restated context, and facts with no bearing on future work are no-ops, not memories.\n- Granularity examples (vague \u2192 reject; sharp \u2192 save):\n - \"user communicates in Chinese\" \u2192 \"user reads English with effort \u2014 write memory and handoff content in Chinese prose\".\n - \"user works on the desktop project\" \u2192 \"desktop profile loads npm-installed plugin copies \u2014 source edits need a release before they take effect\".\n - \"fixed fcitx5 by setting GTK_IM_MODULE tonight\" \u2192 reject (one-off applied fix, already on disk, no recurrence evidence).\n- You cannot read or write source files, call agents, use MCP, use the network, or mutate the harness directly. memory_propose is the only finish tool and never writes state itself.\n- If no durable memory is justified, call memory_propose with edits=[].\n- Always finish by calling memory_propose. Do not return unproposed prose.";
65
65
  /** Build the frozen memory manifest from a merged harness view. */
66
66
  export declare function buildMemoryManifest(state: HarnessState): MemoryManifestEntry[];
67
67
  /** Render a bounded manifest index; full bodies remain available through memory_search. */
@@ -1,5 +1,6 @@
1
1
  import { createAssistantMessage, createToolResultMessage, createUserMessage, } from "@deepseek-ai/dsh-llm";
2
2
  import { requestScopeApproval } from "./approval.js";
3
+ import { fingerprintMemoryBatch, isDeclinedRepeat, loadDeclinedMemory, recordDeclinedMemory, } from "./declines.js";
3
4
  import { streamModelTurn } from "./llm-text.js";
4
5
  import { EVOLVE_MESSAGE_SOURCE } from "./message-source.js";
5
6
  import { extractJsonObject, parseJsonCandidate } from "./plan.js";
@@ -91,12 +92,13 @@ Rules:
91
92
  - Every edit must have kind=memory, an explicit targetScope, and a coherent blastRadius.
92
93
  - targetScope=local is session staging. project persists within the current repository. global persists across projects. project/global require human approval before application.
93
94
  - update/delete/archive must name an existing id from the same targetScope. Prefer archive over delete when the fact is obsolete but should remain restorable.
94
- - Do not remember: code structure or file paths re-readable from the repository; git history, diffs, commit hashes, or CI logs; content already written in project instruction files; temporary session state, one-off debugging trails, or current task progress; model-only speculation without conversation evidence.
95
+ - Do not remember: code structure or file paths re-readable from the repository; git history, diffs, commit hashes, or CI logs; content already written in project instruction files; temporary session state, one-off debugging trails, or current task progress; model-only speculation without conversation evidence; an already-applied troubleshooting fix with no recurrence evidence (the fixed state on disk is its own record).
95
96
  - Be specific enough to act on: prefer a fact that tells the next session what to DO over a summary of what happened. If the fact is obvious from the conversation itself with no future action attached, skip it.
96
97
  - Save only what is surprising or non-obvious: routine chatter, restated context, and facts with no bearing on future work are no-ops, not memories.
97
98
  - Granularity examples (vague → reject; sharp → save):
98
99
  - "user communicates in Chinese" → "user reads English with effort — write memory and handoff content in Chinese prose".
99
100
  - "user works on the desktop project" → "desktop profile loads npm-installed plugin copies — source edits need a release before they take effect".
101
+ - "fixed fcitx5 by setting GTK_IM_MODULE tonight" → reject (one-off applied fix, already on disk, no recurrence evidence).
100
102
  - You cannot read or write source files, call agents, use MCP, use the network, or mutate the harness directly. memory_propose is the only finish tool and never writes state itself.
101
103
  - If no durable memory is justified, call memory_propose with edits=[].
102
104
  - Always finish by calling memory_propose. Do not return unproposed prose.`;
@@ -338,6 +340,13 @@ export async function applyMemoryExtractionProposal(ctx, engine, proposal, optio
338
340
  const baseline = options.baselines[scope];
339
341
  if (!baseline)
340
342
  throw new Error(`memory agent has no ${scope} baseline`);
343
+ const scopeEdits = proposal.edits.filter((edit) => edit.targetScope === scope);
344
+ const fingerprint = fingerprintMemoryBatch(scope, scopeEdits);
345
+ if (isDeclinedRepeat(loadDeclinedMemory(engine.baseDir), scope, fingerprint)) {
346
+ ctx.logger("continual-evolve").info(`memory ${scope} proposal repeat-suppressed (declined before, no popup)`);
347
+ declinedScopes.push(scope);
348
+ continue;
349
+ }
341
350
  const decisionKey = options.decisionCursor === undefined
342
351
  ? undefined
343
352
  : memoryScopeDecisionKey(options.decisionCursor, scope, proposal.edits.filter((edit) => edit.targetScope === scope));
@@ -355,8 +364,15 @@ export async function applyMemoryExtractionProposal(ctx, engine, proposal, optio
355
364
  options.onScopeDecision?.(decisionKey, approved);
356
365
  if (approved === "approved")
357
366
  approvedScopes.add(scope);
358
- else
367
+ else {
359
368
  declinedScopes.push(scope);
369
+ try {
370
+ recordDeclinedMemory(engine.baseDir, scope, scopeEdits, compactText(scopeEdits[0]?.title ?? proposal.summary, 120));
371
+ }
372
+ catch (error) {
373
+ ctx.logger("continual-evolve").warn(`declined-memory ledger write failed: ${error instanceof Error ? error.message : String(error)}`);
374
+ }
375
+ }
360
376
  }
361
377
  const results = [];
362
378
  const applied = [];
@@ -190,8 +190,11 @@ export async function executeWrapupCommand(ctx, engine, invocation, policy = DEF
190
190
  questions: [
191
191
  {
192
192
  id: questionId,
193
- question: `wrapup:条目「${candidate.title}」未被全局覆盖且源自真实对话,直接归档会隐藏它(数据保留、可恢复)。确认归档?`,
194
- options: [{ label: "归档" }, { label: "保留" }],
193
+ question: `wrapup 确认归档:条目「${candidate.title}」\n未被全局覆盖且源自真实对话。归档后不再注入,数据保留、可恢复。`,
194
+ options: [
195
+ { label: "归档", description: "隐藏但可恢复" },
196
+ { label: "保留", description: "继续注入" },
197
+ ],
195
198
  },
196
199
  ],
197
200
  agent: invocation.agent,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-continual-evolve",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
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": {