dsh-continual-evolve 0.4.0 → 0.6.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 +12 -6
- package/README.zh.md +12 -6
- package/lib/apply.d.ts +0 -2
- package/lib/apply.js +18 -8
- package/lib/auto.d.ts +8 -6
- package/lib/auto.js +42 -26
- package/lib/autocase.d.ts +38 -0
- package/lib/autocase.js +76 -0
- package/lib/benchmark-command.js +25 -1
- package/lib/benchmark.d.ts +0 -5
- package/lib/benchmark.js +1 -24
- package/lib/command.d.ts +2 -0
- package/lib/command.js +41 -5
- package/lib/consolidate.d.ts +90 -0
- package/lib/consolidate.js +180 -0
- package/lib/failures.d.ts +4 -1
- package/lib/failures.js +2 -1
- package/lib/index.d.ts +12 -0
- package/lib/index.js +11 -1
- package/lib/inject.d.ts +13 -17
- package/lib/inject.js +49 -37
- package/lib/mount.js +9 -0
- package/lib/promotion.d.ts +38 -6
- package/lib/promotion.js +93 -24
- package/lib/rollback.js +7 -0
- package/lib/search.d.ts +83 -0
- package/lib/search.js +136 -0
- package/lib/service.js +74 -1
- package/lib/state.d.ts +0 -12
- package/lib/state.js +4 -19
- package/lib/store.js +3 -1
- package/lib/tool.js +8 -1
- package/lib/types.d.ts +22 -0
- package/lib/types.js +22 -0
- package/lib/usage.d.ts +1 -10
- package/lib/usage.js +0 -17
- package/lib/validate.d.ts +10 -3
- package/lib/validate.js +52 -5
- package/lib/wrapup-command.js +17 -1
- package/lib/wrapup.d.ts +25 -1
- package/lib/wrapup.js +57 -6
- package/package.json +1 -1
package/lib/tool.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
2
2
|
import { formatHarnessStateForPrompt } from "./render.js";
|
|
3
3
|
import { requireGlobalApproval } from "./approval.js";
|
|
4
|
+
import { CONFLICT_WARN_SCORE, buildConflictNotice, mostSimilarEntry } from "./promotion.js";
|
|
4
5
|
import { entrySourceOf } from "./source.js";
|
|
5
6
|
import { getUsageCount, loadUsage } from "./usage.js";
|
|
6
7
|
import { buildEvolveCompleteEvent, emitEvolveComplete } from "./evolve-event.js";
|
|
@@ -72,7 +73,13 @@ export function registerEvolveTools(ctx, engine, opts) {
|
|
|
72
73
|
execute: async (args, exec) => {
|
|
73
74
|
const scope = scopeOf(args.global, "local");
|
|
74
75
|
if (scope === "global" && opts.requireGlobalApproval) {
|
|
75
|
-
|
|
76
|
+
// Informed approval: surface a similarity hit against the
|
|
77
|
+
// existing global store BEFORE the human decides — the
|
|
78
|
+
// engine's write-time guard still has the final say.
|
|
79
|
+
const globalState = engine.load("global", undefined);
|
|
80
|
+
const hit = mostSimilarEntry(Object.values(globalState.entries[args.kind]), args.title ?? "", args.content ?? "", CONFLICT_WARN_SCORE);
|
|
81
|
+
const conflictNote = hit ? ` ⚠️ ${buildConflictNotice(hit)}——建议改用 evolve_update` : "";
|
|
82
|
+
await requireGlobalApproval(ctx, exec.agent, exec.signal, `evolve_add ${args.kind} "${args.title}" → 跨会话全局 store${conflictNote}`);
|
|
76
83
|
}
|
|
77
84
|
const edit = {
|
|
78
85
|
action: "create",
|
package/lib/types.d.ts
CHANGED
|
@@ -57,6 +57,28 @@ export declare const PROMOTED_AT_KEY = "promotedAt";
|
|
|
57
57
|
* cross-session copy back to the session it was distilled from.
|
|
58
58
|
*/
|
|
59
59
|
export declare const SOURCED_FROM_KEY = "sourcedFromLocal";
|
|
60
|
+
/**
|
|
61
|
+
* Metadata key stamped on a GLOBAL entry that was created despite a
|
|
62
|
+
* moderate-similarity overlap with an existing entry (write-time conflict
|
|
63
|
+
* guard, warn tier): `<kind>:<id>:<score>` — points at the entry it may
|
|
64
|
+
* duplicate so wrapup/fate can consolidate later. Creates at/above the block
|
|
65
|
+
* threshold never reach this stamp; they are rejected outright.
|
|
66
|
+
*/
|
|
67
|
+
export declare const CONFLICT_HINT_KEY = "conflictHint";
|
|
68
|
+
/**
|
|
69
|
+
* Negative-valence counter (P1 效价反馈): how many times an assessment marked
|
|
70
|
+
* this entry contradicted by newer evidence (user correction, overridden
|
|
71
|
+
* fact). Injection ranking demotes entries with a positive counter and the
|
|
72
|
+
* wrapup/fate assessors see the count so they prefer archiving the entry.
|
|
73
|
+
*/
|
|
74
|
+
export declare const VALENCE_NEGATIVE_KEY = "valenceNegative";
|
|
75
|
+
/**
|
|
76
|
+
* Provenance of entries merged INTO this one by the consolidation command
|
|
77
|
+
* (P1 反膨胀): array of `<kind>:<id>` — the near-duplicate sources whose
|
|
78
|
+
* content was appended before they archived. The trail keeps an unarchive of
|
|
79
|
+
* the source explainable and lets audits find the merge provenance.
|
|
80
|
+
*/
|
|
81
|
+
export declare const MERGED_FROM_KEY = "mergedFrom";
|
|
60
82
|
/**
|
|
61
83
|
* True when the entry is archived (hidden from injection, restorable).
|
|
62
84
|
* Absent or empty archivedAt means the entry is active.
|
package/lib/types.js
CHANGED
|
@@ -43,6 +43,28 @@ export const PROMOTED_AT_KEY = "promotedAt";
|
|
|
43
43
|
* cross-session copy back to the session it was distilled from.
|
|
44
44
|
*/
|
|
45
45
|
export const SOURCED_FROM_KEY = "sourcedFromLocal";
|
|
46
|
+
/**
|
|
47
|
+
* Metadata key stamped on a GLOBAL entry that was created despite a
|
|
48
|
+
* moderate-similarity overlap with an existing entry (write-time conflict
|
|
49
|
+
* guard, warn tier): `<kind>:<id>:<score>` — points at the entry it may
|
|
50
|
+
* duplicate so wrapup/fate can consolidate later. Creates at/above the block
|
|
51
|
+
* threshold never reach this stamp; they are rejected outright.
|
|
52
|
+
*/
|
|
53
|
+
export const CONFLICT_HINT_KEY = "conflictHint";
|
|
54
|
+
/**
|
|
55
|
+
* Negative-valence counter (P1 效价反馈): how many times an assessment marked
|
|
56
|
+
* this entry contradicted by newer evidence (user correction, overridden
|
|
57
|
+
* fact). Injection ranking demotes entries with a positive counter and the
|
|
58
|
+
* wrapup/fate assessors see the count so they prefer archiving the entry.
|
|
59
|
+
*/
|
|
60
|
+
export const VALENCE_NEGATIVE_KEY = "valenceNegative";
|
|
61
|
+
/**
|
|
62
|
+
* Provenance of entries merged INTO this one by the consolidation command
|
|
63
|
+
* (P1 反膨胀): array of `<kind>:<id>` — the near-duplicate sources whose
|
|
64
|
+
* content was appended before they archived. The trail keeps an unarchive of
|
|
65
|
+
* the source explainable and lets audits find the merge provenance.
|
|
66
|
+
*/
|
|
67
|
+
export const MERGED_FROM_KEY = "mergedFrom";
|
|
46
68
|
/**
|
|
47
69
|
* True when the entry is archived (hidden from injection, restorable).
|
|
48
70
|
* Absent or empty archivedAt means the entry is active.
|
package/lib/usage.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { RefinementKind } from "./types.js";
|
|
2
2
|
export interface UsageStore {
|
|
3
3
|
/** Injection count per entry key (`kind:id`). */
|
|
4
4
|
counts: Record<string, number>;
|
|
@@ -33,13 +33,4 @@ export declare function recordInjection(baseDir: string, injectedKeys: readonly
|
|
|
33
33
|
* has never been injected (absent from the store).
|
|
34
34
|
*/
|
|
35
35
|
export declare function getUsageCount(store: UsageStore, kind: RefinementKind, id: string): number;
|
|
36
|
-
/**
|
|
37
|
-
* Find entries with zero injection usage. Returns `{kind, id, title}` for
|
|
38
|
-
* each entry that has never been injected — prime candidates for archival.
|
|
39
|
-
*/
|
|
40
|
-
export declare function zeroUsageEntries(state: HarnessState, store: UsageStore): {
|
|
41
|
-
kind: RefinementKind;
|
|
42
|
-
id: string;
|
|
43
|
-
title: string;
|
|
44
|
-
}[];
|
|
45
36
|
//# sourceMappingURL=usage.d.ts.map
|
package/lib/usage.js
CHANGED
|
@@ -95,21 +95,4 @@ export function recordInjection(baseDir, injectedKeys, sessionId) {
|
|
|
95
95
|
export function getUsageCount(store, kind, id) {
|
|
96
96
|
return store.counts[usageKey(kind, id)] ?? 0;
|
|
97
97
|
}
|
|
98
|
-
/**
|
|
99
|
-
* Find entries with zero injection usage. Returns `{kind, id, title}` for
|
|
100
|
-
* each entry that has never been injected — prime candidates for archival.
|
|
101
|
-
*/
|
|
102
|
-
export function zeroUsageEntries(state, store) {
|
|
103
|
-
const results = [];
|
|
104
|
-
for (const kind of Object.keys(state.entries)) {
|
|
105
|
-
for (const entry of Object.values(state.entries[kind])) {
|
|
106
|
-
if (entry.scope !== "local")
|
|
107
|
-
continue;
|
|
108
|
-
if (getUsageCount(store, kind, entry.id) === 0) {
|
|
109
|
-
results.push({ kind, id: entry.id, title: entry.title });
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
return results;
|
|
114
|
-
}
|
|
115
98
|
//# sourceMappingURL=usage.js.map
|
package/lib/validate.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* the base system prompt, required fields per action, and the executable
|
|
5
5
|
* contract skill entries must carry.
|
|
6
6
|
*/
|
|
7
|
-
import type { HarnessScope, RefinementEdit } from "./types.js";
|
|
7
|
+
import type { HarnessEntry, HarnessScope, RefinementEdit } from "./types.js";
|
|
8
8
|
export declare const BASE_SYSTEM_PROMPT_ID = "base_system_prompt";
|
|
9
9
|
/**
|
|
10
10
|
* Gap C2: mechanical check that an edit's declared blast radius is coherent
|
|
@@ -16,6 +16,13 @@ export declare const BASE_SYSTEM_PROMPT_ID = "base_system_prompt";
|
|
|
16
16
|
* incoherently.
|
|
17
17
|
*/
|
|
18
18
|
export declare function validateBlastRadiusScope(scope: HarnessScope, blastRadius: "general" | "project" | "session"): string | undefined;
|
|
19
|
-
/**
|
|
20
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Returns a human-readable failure reason, or undefined when the edit passes.
|
|
21
|
+
*
|
|
22
|
+
* `before` (the entry the edit targets, absent for creates/unknown ids) lets
|
|
23
|
+
* update rules distinguish "carrying the persisted value" from "changing it"
|
|
24
|
+
* — e.g. a rollback inverse re-carries the stored skill_kind, which must
|
|
25
|
+
* pass, while a genuine executable↔guidance switch must not.
|
|
26
|
+
*/
|
|
27
|
+
export declare function validateEdit(edit: RefinementEdit, computedId: string | undefined, scope?: HarnessScope, before?: HarnessEntry): string | undefined;
|
|
21
28
|
//# sourceMappingURL=validate.d.ts.map
|
package/lib/validate.js
CHANGED
|
@@ -20,8 +20,15 @@ export function validateBlastRadiusScope(scope, blastRadius) {
|
|
|
20
20
|
}
|
|
21
21
|
return undefined;
|
|
22
22
|
}
|
|
23
|
-
/**
|
|
24
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Returns a human-readable failure reason, or undefined when the edit passes.
|
|
25
|
+
*
|
|
26
|
+
* `before` (the entry the edit targets, absent for creates/unknown ids) lets
|
|
27
|
+
* update rules distinguish "carrying the persisted value" from "changing it"
|
|
28
|
+
* — e.g. a rollback inverse re-carries the stored skill_kind, which must
|
|
29
|
+
* pass, while a genuine executable↔guidance switch must not.
|
|
30
|
+
*/
|
|
31
|
+
export function validateEdit(edit, computedId, scope, before) {
|
|
25
32
|
if (!ACTIONS.has(edit.action)) {
|
|
26
33
|
return `unsupported action ${String(edit.action)}`;
|
|
27
34
|
}
|
|
@@ -46,10 +53,25 @@ export function validateEdit(edit, computedId, scope) {
|
|
|
46
53
|
if (edit.action === "archive") {
|
|
47
54
|
return undefined;
|
|
48
55
|
}
|
|
49
|
-
|
|
50
|
-
|
|
56
|
+
// Create needs the full payload (nothing to fall back to). Update may
|
|
57
|
+
// carry any subset — apply merges with `?? before` — but must carry at
|
|
58
|
+
// least one change (review audit 2026-08-28 B3/B5: the old blanket
|
|
59
|
+
// "update requires title and content" broke every partial-update path:
|
|
60
|
+
// skill archive/demote, consolidate, and the evolve_update tool contract
|
|
61
|
+
// all construct payload-subset updates).
|
|
62
|
+
if (edit.action === "create" && (!edit.title || !edit.content)) {
|
|
63
|
+
return "create requires title and content";
|
|
64
|
+
}
|
|
65
|
+
if (edit.action === "update" &&
|
|
66
|
+
edit.title === undefined &&
|
|
67
|
+
edit.content === undefined &&
|
|
68
|
+
edit.path === undefined &&
|
|
69
|
+
edit.metadata === undefined &&
|
|
70
|
+
edit.reference === undefined &&
|
|
71
|
+
edit.arguments === undefined) {
|
|
72
|
+
return "update carries no changes";
|
|
51
73
|
}
|
|
52
|
-
if (edit.action
|
|
74
|
+
if (edit.action === "create" && edit.kind === "skill") {
|
|
53
75
|
// Guidance skills are SKILL.md documents: no python reference (a
|
|
54
76
|
// reference on a guidance skill would be an invented contract) and
|
|
55
77
|
// no arguments contract. Executable skills keep the full contract.
|
|
@@ -75,6 +97,31 @@ export function validateEdit(edit, computedId, scope) {
|
|
|
75
97
|
return contentProblems.join("; ");
|
|
76
98
|
}
|
|
77
99
|
}
|
|
100
|
+
if (edit.action === "update" && edit.kind === "skill") {
|
|
101
|
+
// skill_kind is immutable on update: a switch without the matching
|
|
102
|
+
// contract pair would leave an invalid entry (guidance carrying a
|
|
103
|
+
// python reference, or an executable with none). Re-carrying the
|
|
104
|
+
// persisted value (rollback inverses do) passes. Recreate the entry
|
|
105
|
+
// to change the kind.
|
|
106
|
+
if (edit.skill_kind !== undefined && before !== undefined && before.skill_kind !== edit.skill_kind) {
|
|
107
|
+
return "update cannot change skill_kind — delete and recreate the entry instead";
|
|
108
|
+
}
|
|
109
|
+
// A carried non-empty reference replaces the contract wholesale, so it
|
|
110
|
+
// must be a complete executable contract on its own. Empty references
|
|
111
|
+
// (guidance skills) and absent payloads pass through — apply keeps
|
|
112
|
+
// the persisted values.
|
|
113
|
+
if (edit.reference !== undefined && Object.keys(edit.reference).length > 0) {
|
|
114
|
+
const contractError = validateSkillContract(edit);
|
|
115
|
+
if (contractError)
|
|
116
|
+
return contractError;
|
|
117
|
+
}
|
|
118
|
+
if (edit.content !== undefined) {
|
|
119
|
+
const contentProblems = validateSkillEntryContent(edit.content);
|
|
120
|
+
if (contentProblems.length > 0) {
|
|
121
|
+
return contentProblems.join("; ");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
78
125
|
return undefined;
|
|
79
126
|
}
|
|
80
127
|
function validateSkillContract(edit) {
|
package/lib/wrapup-command.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { questionServiceOf, requireGlobalApproval } from "./approval.js";
|
|
2
|
-
import { assessLocalEntries, candidateKey, filterPromotable, listLocalCandidates, splitArchiveGuards, splitPromoteBlocked, splitPromoteProposals, wholePromoteProposals } from "./wrapup.js";
|
|
2
|
+
import { assessLocalEntries, candidateKey, filterPromotable, listLocalCandidates, splitArchiveGuards, splitPromoteBlocked, splitPromoteProposals, valenceStampProposal, wholePromoteProposals } from "./wrapup.js";
|
|
3
3
|
import { DEFAULT_PROMOTION_POLICY } from "./promotion.js";
|
|
4
4
|
function success(text) {
|
|
5
5
|
return { kind: "success", text };
|
|
@@ -206,6 +206,22 @@ export async function executeWrapupCommand(ctx, engine, invocation, policy = DEF
|
|
|
206
206
|
applied.push(`kept ${item.key} — user declined the archive`);
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
|
+
// 7. Valence stamps (P1 效价反馈): keep-verdict entries the assessor marked
|
|
210
|
+
// contradicted get their negative counter bumped — the entry stays live
|
|
211
|
+
// but sinks in injection ranking and the next assessment prefers
|
|
212
|
+
// archiving it. Deterministic local action, no approval needed.
|
|
213
|
+
for (const item of keepItems) {
|
|
214
|
+
if (!item.contradicted)
|
|
215
|
+
continue;
|
|
216
|
+
const candidate = byKey.get(item.key);
|
|
217
|
+
if (!candidate)
|
|
218
|
+
continue;
|
|
219
|
+
const proposal = valenceStampProposal(item, candidate);
|
|
220
|
+
if (!proposal)
|
|
221
|
+
continue;
|
|
222
|
+
const result = engine.apply("local", sessionId, proposal, { scope: "local", baselineState: localState });
|
|
223
|
+
applied.push(`contradicted stamp ${item.key} → valenceNegative=${candidate.negativeCount + 1} (${result.id})`);
|
|
224
|
+
}
|
|
209
225
|
lines.push(...(applied.length > 0 ? applied : ["(no changes applied — all entries kept)"]));
|
|
210
226
|
return success(lines.join("\n"));
|
|
211
227
|
}
|
package/lib/wrapup.d.ts
CHANGED
|
@@ -40,6 +40,14 @@ export interface WrapupItem {
|
|
|
40
40
|
title: string;
|
|
41
41
|
content: string;
|
|
42
42
|
};
|
|
43
|
+
/**
|
|
44
|
+
* Valence signal (P1 效价反馈): the assessor found trajectory or global
|
|
45
|
+
* evidence contradicting this entry's content (user correction, overridden
|
|
46
|
+
* fact). Strictly parsed — only an explicit JSON `true` sets it. Keep
|
|
47
|
+
* verdicts with this flag get a negative-valence stamp; the assessor is
|
|
48
|
+
* instructed to prefer "archive" for contradicted entries.
|
|
49
|
+
*/
|
|
50
|
+
contradicted?: boolean;
|
|
43
51
|
}
|
|
44
52
|
/** A real global entry worth showing the assessor for the same topic. */
|
|
45
53
|
export interface GlobalHint {
|
|
@@ -87,6 +95,13 @@ export interface WrapupCandidate {
|
|
|
87
95
|
* The assessor is instructed to prefer "archive" for stale entries.
|
|
88
96
|
*/
|
|
89
97
|
stale: boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Negative-valence counter (P1 效价反馈): how many prior assessments marked
|
|
100
|
+
* this entry contradicted (metadata {@link VALENCE_NEGATIVE_KEY}). Shown to
|
|
101
|
+
* the assessor so it can prefer "archive"; also demotes the entry in
|
|
102
|
+
* injection ranking (inject.ts rankEntries).
|
|
103
|
+
*/
|
|
104
|
+
negativeCount: number;
|
|
90
105
|
}
|
|
91
106
|
export declare function candidateKey(kind: RefinementKind, id: string): string;
|
|
92
107
|
/**
|
|
@@ -208,7 +223,16 @@ export declare function splitPromoteProposals(item: WrapupItem, candidate: Wrapu
|
|
|
208
223
|
global: RefinementProposal;
|
|
209
224
|
localStamp: (createdId: string) => RefinementProposal;
|
|
210
225
|
};
|
|
211
|
-
|
|
226
|
+
/**
|
|
227
|
+
* Valence stamp (P1 效价反馈): a keep-verdict item the assessor marked
|
|
228
|
+
* contradicted gets its negative-valence counter bumped — the entry stays
|
|
229
|
+
* live but sinks in injection ranking and the next assessment prefers
|
|
230
|
+
* archiving it. Promotes pass untouched (the human-approved global copy is
|
|
231
|
+
* the live truth); archives don't need the stamp (they already leave
|
|
232
|
+
* injection). Undefined when nothing applies.
|
|
233
|
+
*/
|
|
234
|
+
export declare function valenceStampProposal(item: WrapupItem, candidate: WrapupCandidate): RefinementProposal | undefined;
|
|
235
|
+
export declare const WRAPUP_ASSESS_SYSTEM_PROMPT = "You are the /evolve session wrap-up assessor.\nA session is ending and its local harness entries need a fate. Classify each\nlisted entry exactly once:\n\n- \"promote\" \u2014 the content is a stable, durable, CROSS-SESSION reusable lesson:\n a durable user preference, a project-level fact or convention, a reusable\n procedure or skill. Future sessions would benefit from seeing it.\n- \"archive\" \u2014 the content is session-specific task progress, one-off noise,\n superseded or obsolete, or already covered by the global store (note\n \"covered globally\" in the reason), or stale (old + never injected \u2014 note\n \"stale (injectionCount=0, recency low)\" in the reason), or CONTRADICTED \u2014\n newer evidence in the trajectory or the global store disproves or overrides\n the entry's content (note what contradicts it in the reason).\n- \"keep\" \u2014 still actively useful to this session, or genuinely uncertain.\n\nRules:\n- CONTRADICTED: when evidence contradicts an entry, set \"contradicted\": true\n on that item (besides the verdict). Entries marked \"(contradicted N\u00D7\n before)\" were contradicted in earlier assessments \u2014 prefer \"archive\" for\n them unless you have concrete evidence the content is valid again.\n- When an entry is marked \"covered globally\" in the listing, prefer \"archive\"\n or \"keep\" over \"promote\" \u2014 promoting a duplicate gains nothing.\n- When an entry is marked \"stale\" (injectionCount=0 and low recency), prefer\n \"archive\" \u2014 the entry has never been used and is old, so it is unlikely to\n be needed again. Only \"keep\" if the content is clearly valuable despite low\n usage (e.g. a safety policy that rarely triggers but is critical).\n- Do not promote local task state, work-in-progress notes, or content tied to\n one session's ephemeral details.\n- Skills: only \"promote\" a skill entry that is a genuinely reusable procedure\n meeting the DSH skill quality standard; one-off workflows are \"archive\" or\n \"keep\".\n- SPLIT PROMOTION: when an entry mixes a stable, cross-session-reusable part\n WITH session-specific snapshot details, do NOT promote it whole. Instead\n give verdict \"archive\" WITH a \"promote\" sub-object holding a CLEANED\n version of only the durable part (a stable title + the persistent facts,\n stripped of dates/states/one-off figures). Ephemeral snapshot content stays\n out of the sub-object \u2014 it is left behind in the archive. A sub-object is\n only meaningful on \"archive\" verdicts.\n\nReturn JSON only:\n{\n \"rationale\": \"one or two sentences\",\n \"items\": [\n {\"key\": \"memory:foo\", \"verdict\": \"promote|archive|keep\", \"reason\": \"why\"},\n {\"key\": \"memory:bar\", \"verdict\": \"archive\", \"reason\": \"why\", \"contradicted\": true,\n \"promote\": {\"title\": \"cleaned stable title\", \"content\": \"cleaned durable part only\"}}\n ]\n}\nOnly keys from the provided list are allowed; any entry you omit defaults to \"keep\".";
|
|
212
236
|
export interface AssessOptions {
|
|
213
237
|
/** Output token budget for the assessment call. */
|
|
214
238
|
maxOutputTokens?: number;
|
package/lib/wrapup.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { ARCHIVED_AT_KEY, PROMOTED_AT_KEY, PROMOTED_TO_KEY, SOURCE_SEQS_KEY, SOURCE_SESSION_KEY, SOURCED_FROM_KEY, isArchived } from "./types.js";
|
|
1
|
+
import { ARCHIVED_AT_KEY, PROMOTED_AT_KEY, PROMOTED_TO_KEY, SOURCE_SEQS_KEY, SOURCE_SESSION_KEY, SOURCED_FROM_KEY, VALENCE_NEGATIVE_KEY, isArchived } from "./types.js";
|
|
2
2
|
import { extractJsonObject } from "./plan.js";
|
|
3
3
|
import { compactText } from "./render.js";
|
|
4
4
|
import { streamText } from "./llm-text.js";
|
|
5
5
|
import { getUsageCount, loadUsage } from "./usage.js";
|
|
6
6
|
import { recencyScore } from "./inject.js";
|
|
7
|
-
import { DEFAULT_PROMOTION_POLICY, mostSimilarGlobalEntry, projectScopedReason } from "./promotion.js";
|
|
7
|
+
import { DEFAULT_PROMOTION_POLICY, mostSimilarGlobalEntry, projectScopedReason, secretLeakReason } from "./promotion.js";
|
|
8
8
|
export function candidateKey(kind, id) {
|
|
9
9
|
return `${kind}:${id}`;
|
|
10
10
|
}
|
|
@@ -90,6 +90,8 @@ export function listLocalCandidates(state, globalState, baseDir) {
|
|
|
90
90
|
continue;
|
|
91
91
|
const injectionCount = usage ? getUsageCount(usage, kind, entry.id) : 0;
|
|
92
92
|
const stale = injectionCount === 0 && recencyScore(entry, now) < STALE_RECENCY_THRESHOLD;
|
|
93
|
+
const rawNegative = entry.metadata[VALENCE_NEGATIVE_KEY];
|
|
94
|
+
const negativeCount = typeof rawNegative === "number" && Number.isFinite(rawNegative) && rawNegative > 0 ? rawNegative : 0;
|
|
93
95
|
candidates.push({
|
|
94
96
|
kind,
|
|
95
97
|
id: entry.id,
|
|
@@ -102,6 +104,7 @@ export function listLocalCandidates(state, globalState, baseDir) {
|
|
|
102
104
|
globalHints: globalHintsFor(globalState, kind, entry),
|
|
103
105
|
injectionCount,
|
|
104
106
|
stale,
|
|
107
|
+
negativeCount,
|
|
105
108
|
});
|
|
106
109
|
}
|
|
107
110
|
}
|
|
@@ -136,6 +139,9 @@ export function parseWrapupAssessment(text, candidates) {
|
|
|
136
139
|
continue;
|
|
137
140
|
const verdict = item["verdict"] === "promote" || item["verdict"] === "archive" ? item["verdict"] : "keep";
|
|
138
141
|
const built = { key, verdict, reason: typeof item["reason"] === "string" ? item["reason"] : "" };
|
|
142
|
+
if (item["contradicted"] === true) {
|
|
143
|
+
built.contradicted = true;
|
|
144
|
+
}
|
|
139
145
|
if (verdict === "archive" && typeof item["promote"] === "object" && item["promote"] !== null) {
|
|
140
146
|
const sub = item["promote"];
|
|
141
147
|
const subTitle = typeof sub["title"] === "string" ? sub["title"].trim() : "";
|
|
@@ -189,6 +195,15 @@ export function filterPromotable(items, globalState, candidates, policy = DEFAUL
|
|
|
189
195
|
skipped.push({ key: item.key, reason: scoped });
|
|
190
196
|
continue;
|
|
191
197
|
}
|
|
198
|
+
// Metadata screens too: wholePromoteProposals copies candidate.metadata
|
|
199
|
+
// verbatim into the global entry, so a credential planted in local
|
|
200
|
+
// metadata must not ride the promotion path into the shared store
|
|
201
|
+
// (review audit 2026-08-28 B2).
|
|
202
|
+
const secret = secretLeakReason([candidate.title, candidate.content, JSON.stringify(candidate.metadata ?? {})].join("\n"));
|
|
203
|
+
if (secret) {
|
|
204
|
+
skipped.push({ key: item.key, reason: secret });
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
192
207
|
if (candidate.content.length < policy.minPromoteChars) {
|
|
193
208
|
skipped.push({
|
|
194
209
|
key: item.key,
|
|
@@ -265,6 +280,9 @@ export function splitPromoteBlocked(item, globalState, kind, policy = DEFAULT_PR
|
|
|
265
280
|
const scoped = projectScopedReason(`${item.promote.title}\n${item.promote.content}`, policy);
|
|
266
281
|
if (scoped)
|
|
267
282
|
return `split promotion is ${scoped}`;
|
|
283
|
+
const secret = secretLeakReason([item.promote.title, item.promote.content].join("\n"));
|
|
284
|
+
if (secret)
|
|
285
|
+
return `split promotion blocked: ${secret}`;
|
|
268
286
|
if (item.promote.content.length < policy.minPromoteChars) {
|
|
269
287
|
return `split promotion too thin (${item.promote.content.length} < ${policy.minPromoteChars} chars)`;
|
|
270
288
|
}
|
|
@@ -381,8 +399,34 @@ export function splitPromoteProposals(item, candidate, sessionId) {
|
|
|
381
399
|
}),
|
|
382
400
|
};
|
|
383
401
|
}
|
|
402
|
+
/**
|
|
403
|
+
* Valence stamp (P1 效价反馈): a keep-verdict item the assessor marked
|
|
404
|
+
* contradicted gets its negative-valence counter bumped — the entry stays
|
|
405
|
+
* live but sinks in injection ranking and the next assessment prefers
|
|
406
|
+
* archiving it. Promotes pass untouched (the human-approved global copy is
|
|
407
|
+
* the live truth); archives don't need the stamp (they already leave
|
|
408
|
+
* injection). Undefined when nothing applies.
|
|
409
|
+
*/
|
|
410
|
+
export function valenceStampProposal(item, candidate) {
|
|
411
|
+
if (item.verdict !== "keep" || !item.contradicted)
|
|
412
|
+
return undefined;
|
|
413
|
+
return {
|
|
414
|
+
summary: `wrapup: stamp contradicted valence on ${item.key} (${candidate.negativeCount + 1})`,
|
|
415
|
+
rationale: item.reason,
|
|
416
|
+
expectedOutcome: "The entry's negative-valence counter rises; injection downranks it and the next assessment prefers archiving it.",
|
|
417
|
+
edits: [
|
|
418
|
+
{
|
|
419
|
+
action: "update",
|
|
420
|
+
kind: candidate.kind,
|
|
421
|
+
id: candidate.id,
|
|
422
|
+
title: candidate.title,
|
|
423
|
+
content: candidate.content,
|
|
424
|
+
metadata: { ...candidate.metadata, [VALENCE_NEGATIVE_KEY]: candidate.negativeCount + 1 },
|
|
425
|
+
},
|
|
426
|
+
],
|
|
427
|
+
};
|
|
428
|
+
}
|
|
384
429
|
export const WRAPUP_ASSESS_SYSTEM_PROMPT = `You are the /evolve session wrap-up assessor.
|
|
385
|
-
|
|
386
430
|
A session is ending and its local harness entries need a fate. Classify each
|
|
387
431
|
listed entry exactly once:
|
|
388
432
|
|
|
@@ -392,10 +436,16 @@ listed entry exactly once:
|
|
|
392
436
|
- "archive" — the content is session-specific task progress, one-off noise,
|
|
393
437
|
superseded or obsolete, or already covered by the global store (note
|
|
394
438
|
"covered globally" in the reason), or stale (old + never injected — note
|
|
395
|
-
"stale (injectionCount=0, recency low)" in the reason)
|
|
439
|
+
"stale (injectionCount=0, recency low)" in the reason), or CONTRADICTED —
|
|
440
|
+
newer evidence in the trajectory or the global store disproves or overrides
|
|
441
|
+
the entry's content (note what contradicts it in the reason).
|
|
396
442
|
- "keep" — still actively useful to this session, or genuinely uncertain.
|
|
397
443
|
|
|
398
444
|
Rules:
|
|
445
|
+
- CONTRADICTED: when evidence contradicts an entry, set "contradicted": true
|
|
446
|
+
on that item (besides the verdict). Entries marked "(contradicted N×
|
|
447
|
+
before)" were contradicted in earlier assessments — prefer "archive" for
|
|
448
|
+
them unless you have concrete evidence the content is valid again.
|
|
399
449
|
- When an entry is marked "covered globally" in the listing, prefer "archive"
|
|
400
450
|
or "keep" over "promote" — promoting a duplicate gains nothing.
|
|
401
451
|
- When an entry is marked "stale" (injectionCount=0 and low recency), prefer
|
|
@@ -420,7 +470,7 @@ Return JSON only:
|
|
|
420
470
|
"rationale": "one or two sentences",
|
|
421
471
|
"items": [
|
|
422
472
|
{"key": "memory:foo", "verdict": "promote|archive|keep", "reason": "why"},
|
|
423
|
-
{"key": "memory:bar", "verdict": "archive", "reason": "why",
|
|
473
|
+
{"key": "memory:bar", "verdict": "archive", "reason": "why", "contradicted": true,
|
|
424
474
|
"promote": {"title": "cleaned stable title", "content": "cleaned durable part only"}}
|
|
425
475
|
]
|
|
426
476
|
}
|
|
@@ -442,10 +492,11 @@ export async function assessLocalEntries(ctx, agent, candidates, options = {}) {
|
|
|
442
492
|
const key = candidateKey(candidate.kind, candidate.id);
|
|
443
493
|
const covered = candidate.coveredGlobally ? " (covered globally)" : "";
|
|
444
494
|
const stale = candidate.stale ? ` (stale: injectionCount=${candidate.injectionCount}, recency low)` : "";
|
|
495
|
+
const negated = candidate.negativeCount > 0 ? ` (contradicted ${candidate.negativeCount}× before)` : "";
|
|
445
496
|
const hints = candidate.globalHints.length > 0
|
|
446
497
|
? ` | global≈${candidate.globalHints.map((hint) => hint.id + ":" + hint.title).join(", ")}`
|
|
447
498
|
: "";
|
|
448
|
-
return `- ${key} [${candidate.path}, v${candidate.version}] "${candidate.title}"${covered}${stale}${hints}: ${compactText(candidate.content, 220)}`;
|
|
499
|
+
return `- ${key} [${candidate.path}, v${candidate.version}] "${candidate.title}"${covered}${stale}${negated}${hints}: ${compactText(candidate.content, 220)}`;
|
|
449
500
|
})
|
|
450
501
|
.join("\n");
|
|
451
502
|
const userPrompt = [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-continual-evolve",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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": {
|