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 +3 -3
- package/README.zh.md +3 -3
- package/lib/approval.d.ts +14 -0
- package/lib/approval.js +24 -2
- package/lib/auto.js +24 -2
- package/lib/command.js +1 -1
- package/lib/declines.d.ts +101 -0
- package/lib/declines.js +175 -0
- package/lib/fate.js +8 -5
- package/lib/memory-agent.d.ts +1 -1
- package/lib/memory-agent.js +18 -2
- package/lib/wrapup-command.js +5 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
[](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
[](package.json)
|
|
10
|
-
[]()
|
|
11
11
|
[]()
|
|
12
12
|
[]()
|
|
13
13
|
[]()
|
|
@@ -142,7 +142,7 @@ before aborting.
|
|
|
142
142
|
|
|
143
143
|
```bash
|
|
144
144
|
pnpm install && pnpm build # deps + tsc -> lib/
|
|
145
|
-
pnpm test # vitest (
|
|
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 (
|
|
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
|
[](https://github.com/ZK-Andy/dsh-continual-evolve/actions/workflows/ci.yml)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
[](package.json)
|
|
10
|
-
[]()
|
|
11
11
|
[]()
|
|
12
12
|
[]()
|
|
13
13
|
[]()
|
|
@@ -126,7 +126,7 @@ profile patch 示例:
|
|
|
126
126
|
|
|
127
127
|
```bash
|
|
128
128
|
pnpm install && pnpm build # 依赖 + tsc -> lib/
|
|
129
|
-
pnpm test # vitest(
|
|
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 测试套件(
|
|
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:
|
|
25
|
-
options: [
|
|
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:
|
|
729
|
-
options: [
|
|
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: "
|
|
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
|
package/lib/declines.js
ADDED
|
@@ -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: [
|
|
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("
|
|
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
|
-
"
|
|
133
|
+
"自进化门禁:本会话 local 条目需要归宿处理",
|
|
131
134
|
...lines,
|
|
132
|
-
"
|
|
135
|
+
"提升写入后所有会话可见,归档隐藏但可恢复。是否执行?",
|
|
133
136
|
].join("\n");
|
|
134
137
|
}
|
|
135
138
|
/**
|
package/lib/memory-agent.d.ts
CHANGED
|
@@ -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. */
|
package/lib/memory-agent.js
CHANGED
|
@@ -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 = [];
|
package/lib/wrapup-command.js
CHANGED
|
@@ -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
|
|
194
|
-
options: [
|
|
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.
|
|
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": {
|