dsh-continual-evolve 0.10.4 → 0.10.6

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.
@@ -0,0 +1,114 @@
1
+ /**
2
+ * evolve v2 — the passive read path: the `evolve:memory-index` system-prompt
3
+ * section that puts memory CONTENT in front of the model at session start.
4
+ *
5
+ * Why not just hand out a path: the memory store lives in `<dshHome>/evolve/`,
6
+ * outside the workspace sandbox, so the model's read tool cannot open it.
7
+ * The section therefore carries the entries themselves — full body while the
8
+ * character budget allows, then one-line index rows, then a `evolve_recall`
9
+ * pointer for the rest. Budget is hard: a bounded prompt beats an unbounded one.
10
+ *
11
+ * Session-freeze contract (the prompt-cache guard): the section is computed
12
+ * once per session and reused byte-for-byte afterwards, so the system prompt
13
+ * stays stable within a session and cache reads keep hitting. A memory written
14
+ * mid-session becomes visible in the NEXT session; `evolve_recall` covers the
15
+ * immediate need. See `.agents/notes/proposed/feature/2026-10-01-evolve-v2-passive-read-write.md`.
16
+ */
17
+ import type { HarnessEntry } from "./types.js";
18
+ import { type MemoryRecallType } from "./types.js";
19
+ import type { EvolutionEngine } from "./service.js";
20
+ import { type AgentLike } from "./inject.js";
21
+ /** Hard character budget for the `<memories>` block. */
22
+ export declare const DEFAULT_MEMORY_INDEX_MAX_CHARS = 6000;
23
+ /** Prompt-section order of the memory index (empty 1–499 slot, see ADR). */
24
+ export declare const DEFAULT_MEMORY_SECTION_ORDER = 400;
25
+ /** Sessions whose frozen section text is kept before LRU eviction. */
26
+ export declare const DEFAULT_FROZEN_SESSIONS = 32;
27
+ /** Name of the injected section (unique — duplicate names throw upstream). */
28
+ export declare const MEMORY_SECTION_NAME = "evolve:memory-index";
29
+ /**
30
+ * Recall-type priority for the budget: durable, behavior-shaping types first.
31
+ * `project` (current work context) > `feedback` (how to behave) > `user` (who
32
+ * the user is) > `reference` (external pointers). Entries without a valid
33
+ * memoryType sink below every typed entry.
34
+ */
35
+ export declare const MEMORY_TYPE_PRIORITY: Record<MemoryRecallType, number>;
36
+ /** The entry's recall type, or undefined when the metadata key is absent/invalid. */
37
+ export declare function memoryTypeOf(entry: HarnessEntry): MemoryRecallType | undefined;
38
+ /**
39
+ * Order memories for injection: recall-type priority first
40
+ * ({@link MEMORY_TYPE_PRIORITY}), then recency (newest first), then the stable
41
+ * dictionary order — deterministic for equal inputs, so a frozen section is
42
+ * reproducible across builds. The input is never mutated.
43
+ */
44
+ export declare function rankMemories(entries: readonly HarnessEntry[], now?: number): HarnessEntry[];
45
+ /** One memory rendered in full, as a list item with the body indented. */
46
+ export declare function memoryFullLine(entry: HarnessEntry): string;
47
+ /** What a budget-bounded memory block shows. */
48
+ export interface MemorySelection {
49
+ /** Entries rendered with their full body, in display order. */
50
+ full: HarnessEntry[];
51
+ /** Entries degraded to a one-line index row (`id` + title hook). */
52
+ indexed: HarnessEntry[];
53
+ /** Entries that fit neither and are only counted. */
54
+ dropped: number;
55
+ /** Entries actually shown (full + indexed), in display order. */
56
+ shown: HarnessEntry[];
57
+ }
58
+ /**
59
+ * Greedy budget fill over {@link rankMemories}: each entry takes the full body
60
+ * while it fits, otherwise degrades to an index row, otherwise is dropped —
61
+ * and because the ranking is fixed, the first entry that fits neither means
62
+ * every later one is dropped too (their rows are no smaller).
63
+ */
64
+ export declare function selectMemoryInjection(entries: readonly HarnessEntry[], maxChars?: number): MemorySelection;
65
+ /** The `evolve_recall` pointer appended when rows had to be degraded or dropped. */
66
+ export declare const MEMORY_OVERFLOW_HINT = "- \u66F4\u591A\u8BB0\u5FC6\u53EF\u7528 evolve_recall \u6309\u9700\u8BFB\u53D6";
67
+ /**
68
+ * Render a selection as the `<memories>` block, or "" when nothing is shown.
69
+ * Full bodies come first in display order, then the degraded index rows, then
70
+ * the `evolve_recall` pointer with the degradation/drop counts.
71
+ */
72
+ export declare function renderMemorySelection(selection: MemorySelection): string;
73
+ /** {@link renderMemorySelection} over a fresh selection (convenience). */
74
+ export declare function formatMemoriesBlock(entries: readonly HarnessEntry[], maxChars?: number): string;
75
+ /** Options for {@link memoryIndexSectionText}. */
76
+ export interface MemoryIndexOptions {
77
+ /** Hard budget for the memories block (default 6000). */
78
+ maxChars?: number;
79
+ /** Include the when_to_save guide (default true). */
80
+ guide?: boolean;
81
+ /** Pin the project layer explicitly (tests/tools); otherwise derived from cwd. */
82
+ projectKey?: string;
83
+ }
84
+ /**
85
+ * Compose the full `evolve:memory-index` section for one assembling agent:
86
+ * the persistent-memory guide (when to save / how to maintain) wrapped around
87
+ * the budget-bounded memories block. With the guide off and no memories the
88
+ * result is "" — the prompt renderer then drops the section, so an empty store
89
+ * keeps costing zero tokens.
90
+ *
91
+ * Injection usage is recorded for everything shown (best-effort; a failure
92
+ * never blocks injection), deduped per session by {@link recordInjection}.
93
+ */
94
+ export declare function memoryIndexSectionText(engine: EvolutionEngine, agent: AgentLike | undefined, opts?: MemoryIndexOptions): string;
95
+ /** A per-session frozen view of the memory section text. */
96
+ export interface FrozenMemorySection {
97
+ /**
98
+ * The section text for this agent: built on first use per `agent.id` and
99
+ * reused byte-for-byte afterwards. `undefined` agent → "" (unbuilt).
100
+ */
101
+ textFor(agent: AgentLike | undefined, build: (agent: AgentLike) => string): string;
102
+ /** Sessions currently frozen (test/observability hook). */
103
+ size(): number;
104
+ /** Drop every frozen session (tests; a restart rebuilds naturally). */
105
+ clear(): void;
106
+ }
107
+ /**
108
+ * Freeze the section text per session id. The frozen value is what keeps the
109
+ * system prompt stable across a session's assemblies (prompt-cache guard);
110
+ * bounded by `maxSessions` with insertion-order (LRU) eviction, so a long-lived
111
+ * process cannot grow without limit.
112
+ */
113
+ export declare function createFrozenMemorySection(maxSessions?: number): FrozenMemorySection;
114
+ //# sourceMappingURL=memory-index.d.ts.map
@@ -0,0 +1,197 @@
1
+ import { MEMORY_TYPE_KEY, isArchived, isMemoryType } from "./types.js";
2
+ import { recordInjection } from "./usage.js";
3
+ import { MEMORY_GUIDE_INTRO, MEMORY_GUIDE_RULES } from "./memory-guide.js";
4
+ import { directoryLine, mergedInjectionState, recencyScore } from "./inject.js";
5
+ /** Hard character budget for the `<memories>` block. */
6
+ export const DEFAULT_MEMORY_INDEX_MAX_CHARS = 6000;
7
+ /** Prompt-section order of the memory index (empty 1–499 slot, see ADR). */
8
+ export const DEFAULT_MEMORY_SECTION_ORDER = 400;
9
+ /** Sessions whose frozen section text is kept before LRU eviction. */
10
+ export const DEFAULT_FROZEN_SESSIONS = 32;
11
+ /** Name of the injected section (unique — duplicate names throw upstream). */
12
+ export const MEMORY_SECTION_NAME = "evolve:memory-index";
13
+ /**
14
+ * Recall-type priority for the budget: durable, behavior-shaping types first.
15
+ * `project` (current work context) > `feedback` (how to behave) > `user` (who
16
+ * the user is) > `reference` (external pointers). Entries without a valid
17
+ * memoryType sink below every typed entry.
18
+ */
19
+ export const MEMORY_TYPE_PRIORITY = {
20
+ project: 0,
21
+ feedback: 1,
22
+ user: 2,
23
+ reference: 3,
24
+ };
25
+ /** Priority of an entry whose metadata carries no valid recall type. */
26
+ const UNTYPED_PRIORITY = 4;
27
+ /** The entry's recall type, or undefined when the metadata key is absent/invalid. */
28
+ export function memoryTypeOf(entry) {
29
+ const value = entry.metadata[MEMORY_TYPE_KEY];
30
+ return isMemoryType(value) ? value : undefined;
31
+ }
32
+ /** Deterministic final tiebreak (matches `inject.ts` ranking). */
33
+ function stableCompare(a, b) {
34
+ return [a.path, a.title, a.id].join("\0").localeCompare([b.path, b.title, b.id].join("\0"));
35
+ }
36
+ /**
37
+ * Order memories for injection: recall-type priority first
38
+ * ({@link MEMORY_TYPE_PRIORITY}), then recency (newest first), then the stable
39
+ * dictionary order — deterministic for equal inputs, so a frozen section is
40
+ * reproducible across builds. The input is never mutated.
41
+ */
42
+ export function rankMemories(entries, now = Date.now()) {
43
+ return [...entries].sort((a, b) => {
44
+ const aType = memoryTypeOf(a);
45
+ const bType = memoryTypeOf(b);
46
+ const priorityDelta = (aType ? MEMORY_TYPE_PRIORITY[aType] : UNTYPED_PRIORITY) - (bType ? MEMORY_TYPE_PRIORITY[bType] : UNTYPED_PRIORITY);
47
+ if (priorityDelta !== 0) {
48
+ return priorityDelta;
49
+ }
50
+ const recencyDelta = recencyScore(b, now) - recencyScore(a, now);
51
+ if (recencyDelta !== 0) {
52
+ return recencyDelta;
53
+ }
54
+ return stableCompare(a, b);
55
+ });
56
+ }
57
+ /** One memory rendered in full, as a list item with the body indented. */
58
+ export function memoryFullLine(entry) {
59
+ const type = memoryTypeOf(entry);
60
+ const body = entry.content.trim().replace(/\n/g, "\n ");
61
+ const head = `- [memory${type ? `:${type}` : ""}:${entry.id}] ${entry.title}`;
62
+ return body.length > 0 ? `${head}\n ${body}` : head;
63
+ }
64
+ /**
65
+ * Greedy budget fill over {@link rankMemories}: each entry takes the full body
66
+ * while it fits, otherwise degrades to an index row, otherwise is dropped —
67
+ * and because the ranking is fixed, the first entry that fits neither means
68
+ * every later one is dropped too (their rows are no smaller).
69
+ */
70
+ export function selectMemoryInjection(entries, maxChars = DEFAULT_MEMORY_INDEX_MAX_CHARS) {
71
+ const visible = entries.filter((entry) => !isArchived(entry));
72
+ const ranked = rankMemories(visible);
73
+ const budget = Math.max(0, maxChars);
74
+ const full = [];
75
+ const indexed = [];
76
+ const shown = [];
77
+ let used = 0;
78
+ let dropped = 0;
79
+ for (const entry of ranked) {
80
+ const fullLine = memoryFullLine(entry);
81
+ if (used + fullLine.length <= budget) {
82
+ full.push(entry);
83
+ shown.push(entry);
84
+ used += fullLine.length;
85
+ continue;
86
+ }
87
+ const indexLine = directoryLine(entry);
88
+ if (used + indexLine.length <= budget) {
89
+ indexed.push(entry);
90
+ shown.push(entry);
91
+ used += indexLine.length;
92
+ continue;
93
+ }
94
+ dropped = ranked.length - shown.length;
95
+ break;
96
+ }
97
+ return { full, indexed, dropped, shown };
98
+ }
99
+ /** The `evolve_recall` pointer appended when rows had to be degraded or dropped. */
100
+ export const MEMORY_OVERFLOW_HINT = "- 更多记忆可用 evolve_recall 按需读取";
101
+ /**
102
+ * Render a selection as the `<memories>` block, or "" when nothing is shown.
103
+ * Full bodies come first in display order, then the degraded index rows, then
104
+ * the `evolve_recall` pointer with the degradation/drop counts.
105
+ */
106
+ export function renderMemorySelection(selection) {
107
+ const { full, indexed, dropped, shown } = selection;
108
+ if (shown.length === 0) {
109
+ return "";
110
+ }
111
+ const lines = [];
112
+ for (const entry of full) {
113
+ lines.push(memoryFullLine(entry));
114
+ }
115
+ for (const entry of indexed) {
116
+ lines.push(directoryLine(entry));
117
+ }
118
+ const omitted = [];
119
+ if (indexed.length > 0) {
120
+ omitted.push(`${indexed.length} 条仅列索引`);
121
+ }
122
+ if (dropped > 0) {
123
+ omitted.push(`${dropped} 条未列出`);
124
+ }
125
+ lines.push(omitted.length > 0 ? `${MEMORY_OVERFLOW_HINT}(${omitted.join(",")})` : MEMORY_OVERFLOW_HINT);
126
+ return `<memories>\n${lines.join("\n")}\n</memories>`;
127
+ }
128
+ /** {@link renderMemorySelection} over a fresh selection (convenience). */
129
+ export function formatMemoriesBlock(entries, maxChars = DEFAULT_MEMORY_INDEX_MAX_CHARS) {
130
+ return renderMemorySelection(selectMemoryInjection(entries, maxChars));
131
+ }
132
+ /**
133
+ * Compose the full `evolve:memory-index` section for one assembling agent:
134
+ * the persistent-memory guide (when to save / how to maintain) wrapped around
135
+ * the budget-bounded memories block. With the guide off and no memories the
136
+ * result is "" — the prompt renderer then drops the section, so an empty store
137
+ * keeps costing zero tokens.
138
+ *
139
+ * Injection usage is recorded for everything shown (best-effort; a failure
140
+ * never blocks injection), deduped per session by {@link recordInjection}.
141
+ */
142
+ export function memoryIndexSectionText(engine, agent, opts) {
143
+ if (!agent) {
144
+ return "";
145
+ }
146
+ const guide = opts?.guide ?? true;
147
+ const maxChars = opts?.maxChars ?? DEFAULT_MEMORY_INDEX_MAX_CHARS;
148
+ const merged = mergedInjectionState(engine, agent, opts);
149
+ const selection = selectMemoryInjection(Object.values(merged.entries.memory), maxChars);
150
+ const block = renderMemorySelection(selection);
151
+ if (selection.shown.length > 0) {
152
+ try {
153
+ recordInjection(engine.baseDir, selection.shown.map((entry) => `${entry.kind}:${entry.id}`), agent.id);
154
+ }
155
+ catch {
156
+ // Usage recording is diagnostic; never interrupt the injection path.
157
+ }
158
+ }
159
+ if (!guide) {
160
+ return block;
161
+ }
162
+ return [MEMORY_GUIDE_INTRO, block.length > 0 ? block : "<memories>\n(暂无记忆)\n</memories>", MEMORY_GUIDE_RULES].join("\n\n");
163
+ }
164
+ /**
165
+ * Freeze the section text per session id. The frozen value is what keeps the
166
+ * system prompt stable across a session's assemblies (prompt-cache guard);
167
+ * bounded by `maxSessions` with insertion-order (LRU) eviction, so a long-lived
168
+ * process cannot grow without limit.
169
+ */
170
+ export function createFrozenMemorySection(maxSessions = DEFAULT_FROZEN_SESSIONS) {
171
+ const limit = Math.max(1, maxSessions);
172
+ const frozen = new Map();
173
+ return {
174
+ textFor(agent, build) {
175
+ if (!agent) {
176
+ return "";
177
+ }
178
+ const cached = frozen.get(agent.id);
179
+ if (cached !== undefined) {
180
+ return cached;
181
+ }
182
+ const text = build(agent);
183
+ frozen.set(agent.id, text);
184
+ while (frozen.size > limit) {
185
+ const oldest = frozen.keys().next();
186
+ if (oldest.done === true) {
187
+ break;
188
+ }
189
+ frozen.delete(oldest.value);
190
+ }
191
+ return text;
192
+ },
193
+ size: () => frozen.size,
194
+ clear: () => frozen.clear(),
195
+ };
196
+ }
197
+ //# sourceMappingURL=memory-index.js.map
package/lib/tool.d.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  * the engine. `global: true` is required explicitly for cross-session edits.
5
5
  */
6
6
  import type { Context } from "@deepseek-ai/cordis";
7
+ import type { RecordLanguagePreference } from "./record-language.js";
7
8
  import type { HarnessScope } from "./types.js";
8
9
  import type { EvolutionEngine } from "./service.js";
9
10
  /**
@@ -22,6 +23,12 @@ export declare function scopeOf(value: unknown, fallback: HarnessScope): Harness
22
23
  export declare function collectDeleteIds(id: unknown, ids: unknown): string[];
23
24
  export interface ToolGateOptions {
24
25
  requireGlobalApproval: boolean;
26
+ /**
27
+ * Plugin `recordLanguage` preference for the approval dialog copy;
28
+ * `auto`/absent defers to the durable client preference and the session's
29
+ * own user text (see `approval.ts`).
30
+ */
31
+ recordLanguage?: RecordLanguagePreference;
25
32
  }
26
33
  export declare function registerEvolveTools(ctx: Context, engine: EvolutionEngine, opts: ToolGateOptions): void;
27
34
  //# sourceMappingURL=tool.d.ts.map
package/lib/tool.js CHANGED
@@ -83,12 +83,12 @@ function textResult(text) {
83
83
  export function registerEvolveTools(ctx, engine, opts) {
84
84
  ctx.tools.register(defineTool({
85
85
  name: "evolve_list",
86
- description: "List the continual harness state (prompt notes, memories, skills, subagent specs) for the current session (local), the current project (project), or across projects (global).",
86
+ description: "List the harness state (prompt notes, memories, skills, subagent specs) for one store.",
87
87
  parameters: {
88
88
  scope: {
89
89
  type: "string",
90
90
  enum: SCOPES,
91
- description: "Which store to list: 'local' (default), 'project', or 'global'.",
91
+ description: "'local' (default), 'project', or 'global'.",
92
92
  },
93
93
  },
94
94
  output: {
@@ -118,25 +118,25 @@ export function registerEvolveTools(ctx, engine, opts) {
118
118
  }));
119
119
  ctx.tools.register(defineTool({
120
120
  name: "evolve_recall",
121
- description: "Targeted recall over the harness stores: filter memories (or other kinds) by query, kind, scope, and memory type, and read back full content with version, source, and staleness signals. Read-only — it never mutates a store. Prefer this over evolve_list when looking for something specific.",
121
+ description: "Read-only targeted recall across the harness stores: filter by query, kind, scope, and memory type; returns full content with version, source, and staleness signals. Prefer this over evolve_list when looking for something specific.",
122
122
  parameters: {
123
- query: { type: "string", description: "Free-text relevance query. Empty means most-recently-updated first." },
123
+ query: { type: "string", description: "Free-text relevance query. Empty → most-recently-updated first." },
124
124
  kinds: {
125
125
  type: "array",
126
126
  items: { type: "string" },
127
- description: "Entry kinds to search (default memory only).",
127
+ description: "Entry kinds (default memory only).",
128
128
  },
129
129
  scopes: {
130
130
  type: "array",
131
131
  items: { type: "string" },
132
- description: "Stores to search (default local, project, and global).",
132
+ description: "Stores to search (default all three).",
133
133
  },
134
134
  memoryTypes: {
135
135
  type: "array",
136
136
  items: { type: "string" },
137
- description: "Memory-type filter: user, feedback, project, reference (memory entries only).",
137
+ description: "Memory types: user, feedback, project, reference.",
138
138
  },
139
- limit: { type: "number", description: "Maximum hits (default 10, at most 50)." },
139
+ limit: { type: "number", description: "Max hits (default 10, max 50)." },
140
140
  includeArchived: { type: "boolean", description: "Include archived entries (default false)." },
141
141
  },
142
142
  output: {
@@ -157,18 +157,18 @@ export function registerEvolveTools(ctx, engine, opts) {
157
157
  }));
158
158
  ctx.tools.register(defineTool({
159
159
  name: "evolve_add",
160
- description: "Create one harness entry (prompt/memory/skill/subagent). Executable skills require reference {type:python, import, callable} and an arguments contract; guidance skills (skill_kind=guidance) are SKILL.md documents — recurring multi-step workflows — and must NOT carry a reference. Snapshot, version, and history are handled automatically.",
160
+ description: "Create one harness entry (prompt/memory/skill/subagent). Snapshots, versioning, and audit are automatic.",
161
161
  parameters: {
162
162
  kind: { type: "string", enum: ["prompt", "memory", "skill", "subagent"], required: true, description: "Entry kind." },
163
163
  title: { type: "string", required: true, description: "Stable title." },
164
164
  content: { type: "string", required: true, description: "Entry body." },
165
165
  path: { type: "string", description: "Optional grouping path." },
166
- skill_kind: { type: "string", enum: ["executable", "guidance"], description: "For skills: executable (python reference, default) or guidance (SKILL.md document, no reference)." },
167
- reference: { type: "object", additionalProperties: true, description: "For executable skills: {type:'python', import, callable}." },
168
- arguments: { type: "object", additionalProperties: true, description: "For executable skills: accepted input contract." },
169
- memoryType: { type: "string", enum: ["user", "feedback", "project", "reference"], description: "Required for memory: one fact per entry (feedback/project must carry Why + How to apply)." },
170
- scope: { type: "string", enum: SCOPES, description: "Target store: 'local' (default), 'project', or 'global'. Wins over the legacy global flag." },
171
- global: { type: "boolean", description: "Set true to write the cross-session store (requires human approval; only for durable, reusable lessons)." },
166
+ skill_kind: { type: "string", enum: ["executable", "guidance"], description: "Skills: executable (default, needs reference) or guidance (SKILL.md, no reference)." },
167
+ reference: { type: "object", additionalProperties: true, description: "Executable skills: {type:'python', import, callable}." },
168
+ arguments: { type: "object", additionalProperties: true, description: "Executable skills: accepted input contract." },
169
+ memoryType: { type: "string", enum: ["user", "feedback", "project", "reference"], description: "Required for memory (feedback/project need Why + How to apply)." },
170
+ scope: { type: "string", enum: SCOPES, description: "'local' (default), 'project', or 'global'." },
171
+ global: { type: "boolean", description: "Shorthand for scope='global' (requires human approval)." },
172
172
  },
173
173
  output: {
174
174
  schema: { type: "object", additionalProperties: false, properties: { text: { type: "string", required: true } } },
@@ -186,7 +186,7 @@ export function registerEvolveTools(ctx, engine, opts) {
186
186
  const targetState = engine.load(scope, storeId);
187
187
  const hit = mostSimilarEntry(Object.values(targetState.entries[args.kind]), args.title ?? "", args.content ?? "", CONFLICT_WARN_SCORE);
188
188
  const conflictNote = hit ? ` ⚠️ ${buildConflictNotice(hit)}——建议改用 evolve_update` : "";
189
- await requireGlobalApproval(ctx, exec.agent, exec.signal, `evolve_add ${args.kind} "${args.title}" → ${storeLabel(scope)}${conflictNote}`);
189
+ await requireGlobalApproval(ctx, exec.agent, exec.signal, `evolve_add ${args.kind} "${args.title}" → ${storeLabel(scope)}${conflictNote}`, opts.recordLanguage);
190
190
  }
191
191
  const edit = {
192
192
  action: "create",
@@ -216,9 +216,9 @@ export function registerEvolveTools(ctx, engine, opts) {
216
216
  id: { type: "string", required: true, description: "Existing entry id." },
217
217
  title: { type: "string" },
218
218
  content: { type: "string" },
219
- memoryType: { type: "string", enum: ["user", "feedback", "project", "reference"], description: "For memory: (re)classify the entry's recall type." },
220
- scope: { type: "string", enum: SCOPES, description: "Target store: 'local' (default), 'project', or 'global'. Wins over the legacy global flag." },
221
- global: { type: "boolean", description: "Set true to edit the cross-session store (requires human approval)." },
219
+ memoryType: { type: "string", enum: ["user", "feedback", "project", "reference"], description: "Memory: (re)classify the recall type." },
220
+ scope: { type: "string", enum: SCOPES, description: "'local' (default), 'project', or 'global'." },
221
+ global: { type: "boolean", description: "Shorthand for scope='global' (requires human approval)." },
222
222
  },
223
223
  output: {
224
224
  schema: { type: "object", additionalProperties: false, properties: { text: { type: "string", required: true } } },
@@ -228,7 +228,7 @@ export function registerEvolveTools(ctx, engine, opts) {
228
228
  const scope = scopeOf(args.scope ?? args.global, "local");
229
229
  const storeId = storeIdFor(scope, exec);
230
230
  if (needsApproval(scope) && opts.requireGlobalApproval) {
231
- await requireGlobalApproval(ctx, exec.agent, exec.signal, `evolve_update ${args.kind}:${args.id} → ${storeLabel(scope)}`);
231
+ await requireGlobalApproval(ctx, exec.agent, exec.signal, `evolve_update ${args.kind}:${args.id} → ${storeLabel(scope)}`, opts.recordLanguage);
232
232
  }
233
233
  const edit = { action: "update", kind: args.kind, id: args.id };
234
234
  if (args.title !== undefined)
@@ -242,17 +242,17 @@ export function registerEvolveTools(ctx, engine, opts) {
242
242
  }));
243
243
  ctx.tools.register(defineTool({
244
244
  name: "evolve_delete",
245
- description: "Delete harness entries by id. Pass `id` for one entry or `ids` for several — a batch lands as one refinement behind one approval. Literal ids win: entries whose stored id itself carries a scope-like prefix (e.g. a legacy `local:…` id) are addressed verbatim.",
245
+ description: "Delete harness entries: `id` for one, `ids` for a batch (one refinement, one approval). Ids are addressed verbatim, scope-prefixed or not.",
246
246
  parameters: {
247
247
  kind: { type: "string", enum: ["prompt", "memory", "skill", "subagent"], required: true },
248
- id: { type: "string", description: "Existing entry id (single delete; combine with ids or use ids for a batch)." },
248
+ id: { type: "string", description: "One entry id (or use ids for a batch)." },
249
249
  ids: {
250
250
  type: "array",
251
251
  items: { type: "string" },
252
- description: "Existing entry ids for one batch delete (one refinement, one approval).",
252
+ description: "Entry ids for one batch delete.",
253
253
  },
254
- scope: { type: "string", enum: SCOPES, description: "Target store: 'local' (default), 'project', or 'global'. Wins over the legacy global flag." },
255
- global: { type: "boolean", description: "Set true to edit the cross-session store (requires human approval)." },
254
+ scope: { type: "string", enum: SCOPES, description: "'local' (default), 'project', or 'global'." },
255
+ global: { type: "boolean", description: "Shorthand for scope='global' (requires human approval)." },
256
256
  },
257
257
  output: {
258
258
  schema: { type: "object", additionalProperties: false, properties: { text: { type: "string", required: true } } },
@@ -263,7 +263,7 @@ export function registerEvolveTools(ctx, engine, opts) {
263
263
  const storeId = storeIdFor(scope, exec);
264
264
  const ids = collectDeleteIds(args.id, args.ids);
265
265
  if (needsApproval(scope) && opts.requireGlobalApproval) {
266
- await requireGlobalApproval(ctx, exec.agent, exec.signal, `evolve_delete ${args.kind}:${ids.length > 1 ? `${ids.length} entries (${ids.join(", ")})` : ids[0]} → ${storeLabel(scope)}`);
266
+ await requireGlobalApproval(ctx, exec.agent, exec.signal, `evolve_delete ${args.kind}:${ids.length > 1 ? `${ids.length} entries (${ids.join(", ")})` : ids[0]} → ${storeLabel(scope)}`, opts.recordLanguage);
267
267
  }
268
268
  const edits = ids.map((id) => ({ action: "delete", kind: args.kind, id }));
269
269
  return textResult(applyEditsText(engine, scope, storeId, edits, exec.agent, sessionIdOf(exec)));
@@ -271,11 +271,11 @@ export function registerEvolveTools(ctx, engine, opts) {
271
271
  }));
272
272
  ctx.tools.register(defineTool({
273
273
  name: "evolve_rollback",
274
- description: "Deterministically revert a previous refinement by its id (from evolve_list history or the /evolve command).",
274
+ description: "Deterministically revert a previous refinement by its id (from evolve_list history or /evolve).",
275
275
  parameters: {
276
- refinementId: { type: "string", required: true, description: "The refinement id to roll back." },
277
- scope: { type: "string", enum: SCOPES, description: "Target store: 'local' (default), 'project', or 'global'. Wins over the legacy global flag." },
278
- global: { type: "boolean", description: "Set true to roll back a cross-session refinement." },
276
+ refinementId: { type: "string", required: true, description: "Refinement id to roll back." },
277
+ scope: { type: "string", enum: SCOPES, description: "'local' (default), 'project', or 'global'." },
278
+ global: { type: "boolean", description: "Shorthand for scope='global'." },
279
279
  },
280
280
  output: {
281
281
  schema: { type: "object", additionalProperties: false, properties: { text: { type: "string", required: true } } },
@@ -5,5 +5,6 @@ import type { Context } from "@deepseek-ai/cordis";
5
5
  import type { CommandInvocation, CommandResult } from "@deepseek-ai/dsh-commands";
6
6
  import type { EvolutionEngine } from "./service.js";
7
7
  import { type PromotionPolicy } from "./promotion.js";
8
- export declare function executeWrapupCommand(ctx: Context, engine: EvolutionEngine, invocation: CommandInvocation, policy?: PromotionPolicy): Promise<CommandResult>;
8
+ import { type RecordLanguagePreference } from "./record-language.js";
9
+ export declare function executeWrapupCommand(ctx: Context, engine: EvolutionEngine, invocation: CommandInvocation, policy?: PromotionPolicy, recordLanguage?: RecordLanguagePreference): Promise<CommandResult>;
9
10
  //# sourceMappingURL=wrapup-command.d.ts.map
@@ -1,11 +1,13 @@
1
1
  import { questionServiceOf, requireGlobalApproval } from "./approval.js";
2
- import { resolveDialogLanguage, wrapupArchiveCopy } from "./copy.js";
2
+ import { wrapupArchiveCopy } from "./copy.js";
3
3
  import { assessLocalEntries, candidateKey, filterPromotable, listLocalCandidates, splitArchiveGuards, splitPromoteBlocked, splitPromoteProposals, valenceStampProposal, wholePromoteProposals } from "./wrapup.js";
4
4
  import { DEFAULT_PROMOTION_POLICY } from "./promotion.js";
5
+ import { normalizeRecordLanguage, resolveRecordLanguage } from "./record-language.js";
6
+ import { recentUserText } from "./inject.js";
5
7
  function success(text) {
6
8
  return { kind: "success", text };
7
9
  }
8
- export async function executeWrapupCommand(ctx, engine, invocation, policy = DEFAULT_PROMOTION_POLICY) {
10
+ export async function executeWrapupCommand(ctx, engine, invocation, policy = DEFAULT_PROMOTION_POLICY, recordLanguage) {
9
11
  const sessionId = invocation.agent.id;
10
12
  const localState = engine.load("local", sessionId);
11
13
  const globalState = engine.load("global", undefined);
@@ -102,7 +104,7 @@ export async function executeWrapupCommand(ctx, engine, invocation, policy = DEF
102
104
  ].join("\n")}`;
103
105
  let promoteAllowed = true;
104
106
  try {
105
- await requireGlobalApproval(ctx, invocation.agent, invocation.signal, what);
107
+ await requireGlobalApproval(ctx, invocation.agent, invocation.signal, what, recordLanguage);
106
108
  }
107
109
  catch (cause) {
108
110
  promoteAllowed = false;
@@ -187,7 +189,8 @@ export async function executeWrapupCommand(ctx, engine, invocation, policy = DEF
187
189
  const questionId = "evolve-wrapup-archive-review";
188
190
  let archiveConfirmed = false;
189
191
  try {
190
- const copy = wrapupArchiveCopy(candidate.title, resolveDialogLanguage(ctx));
192
+ const copy = wrapupArchiveCopy(candidate.title, normalizeRecordLanguage(recordLanguage) ??
193
+ resolveRecordLanguage({ ctx, trajectoryText: recentUserText(invocation.agent) }));
191
194
  const answer = await userQuestions.ask({
192
195
  questions: [
193
196
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-continual-evolve",
3
- "version": "0.10.4",
3
+ "version": "0.10.6",
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": {
@@ -56,6 +56,7 @@
56
56
  "lint": "oxlint src test",
57
57
  "clean": "rm -rf lib",
58
58
  "check:docs": "tsx scripts/run-gates.ts docs",
59
+ "check:handoff": "tsx scripts/verify-handoff-structure.ts --self-test && tsx scripts/verify-handoff-structure.ts",
59
60
  "check:governance": "tsx scripts/verify-governance.ts"
60
61
  },
61
62
  "peerDependencies": {