dsh-continual-evolve 0.10.5 → 0.11.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.
Files changed (69) hide show
  1. package/README.md +25 -31
  2. package/README.zh.md +18 -21
  3. package/lib/approval.d.ts +19 -6
  4. package/lib/approval.js +73 -12
  5. package/lib/audit-log.d.ts +19 -0
  6. package/lib/audit-log.js +52 -0
  7. package/lib/auto.js +1 -0
  8. package/lib/benchmark-command.d.ts +1 -1
  9. package/lib/benchmark-command.js +1 -1
  10. package/lib/command-util.d.ts +70 -0
  11. package/lib/command-util.js +123 -0
  12. package/lib/command.d.ts +7 -40
  13. package/lib/command.js +25 -459
  14. package/lib/copy.d.ts +4 -41
  15. package/lib/copy.js +0 -102
  16. package/lib/declines.d.ts +2 -0
  17. package/lib/declines.js +4 -0
  18. package/lib/evolve-event.js +2 -1
  19. package/lib/extraction-phase.d.ts +132 -0
  20. package/lib/extraction-phase.js +236 -0
  21. package/lib/index.d.ts +26 -42
  22. package/lib/index.js +24 -34
  23. package/lib/inject.d.ts +13 -119
  24. package/lib/inject.js +12 -260
  25. package/lib/injection-caps.d.ts +4 -0
  26. package/lib/injection-caps.js +4 -0
  27. package/lib/injection-directory.d.ts +29 -0
  28. package/lib/injection-directory.js +78 -0
  29. package/lib/injection-rank.d.ts +85 -0
  30. package/lib/injection-rank.js +161 -0
  31. package/lib/listener.d.ts +45 -0
  32. package/lib/listener.js +199 -0
  33. package/lib/maintenance-commands.d.ts +23 -0
  34. package/lib/maintenance-commands.js +136 -0
  35. package/lib/memory-agent.d.ts +7 -41
  36. package/lib/memory-agent.js +8 -276
  37. package/lib/memory-apply.d.ts +44 -0
  38. package/lib/memory-apply.js +202 -0
  39. package/lib/memory-manifest.d.ts +16 -0
  40. package/lib/memory-manifest.js +70 -0
  41. package/lib/mount-command.js +1 -1
  42. package/lib/notify.d.ts +9 -20
  43. package/lib/notify.js +0 -40
  44. package/lib/plan-command.d.ts +12 -0
  45. package/lib/plan-command.js +38 -0
  46. package/lib/prefix-cache.d.ts +2 -1
  47. package/lib/review-scheduler.d.ts +1 -1
  48. package/lib/review-scheduler.js +2 -10
  49. package/lib/runtime-commands.d.ts +14 -0
  50. package/lib/runtime-commands.js +115 -0
  51. package/lib/service.d.ts +16 -2
  52. package/lib/service.js +74 -1
  53. package/lib/store.d.ts +1 -2
  54. package/lib/store.js +2 -2
  55. package/lib/tool.d.ts +7 -0
  56. package/lib/tool.js +4 -4
  57. package/lib/turn-snapshot.d.ts +7 -1
  58. package/lib/turn-snapshot.js +14 -0
  59. package/lib/types.d.ts +10 -0
  60. package/lib/types.js +6 -0
  61. package/lib/wrapup-candidates.d.ts +127 -0
  62. package/lib/wrapup-candidates.js +161 -0
  63. package/lib/wrapup-command.d.ts +2 -1
  64. package/lib/wrapup-command.js +7 -4
  65. package/lib/wrapup-proposals.d.ts +101 -0
  66. package/lib/wrapup-proposals.js +281 -0
  67. package/lib/wrapup.d.ts +8 -212
  68. package/lib/wrapup.js +4 -438
  69. package/package.json +1 -1
@@ -0,0 +1,136 @@
1
+ /**
2
+ * `/evolve` maintenance and observability subcommands: global-store
3
+ * consolidation, failure aggregation, plugin log inspection, store
4
+ * export/import backup, and entry archive/unarchive.
5
+ */
6
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
7
+ import { ARCHIVED_AT_KEY } from "./types.js";
8
+ import { filterLogBySession, formatLogLine, pluginLogFilePath } from "./logfile.js";
9
+ import { collectFailureSummary, formatFailureSummary } from "./failures.js";
10
+ import { planConsolidation } from "./consolidate.js";
11
+ import { loadUsage } from "./usage.js";
12
+ import { findEntryById, parsePositiveInt, renderResult, stripAngleBrackets, storeIdForCommand, success, error, } from "./command-util.js";
13
+ /** `/evolve archive|unarchive <id> [global|project]` — hide or restore an entry. */
14
+ export async function executeArchiveCommand(_ctx, engine, invocation, sub, parsed, usage) {
15
+ const id = stripAngleBrackets(parsed.rest[0] ?? "");
16
+ if (!id) {
17
+ return error(`${sub} requires an entry id.\n${usage}`);
18
+ }
19
+ const state = engine.load(parsed.scope, storeIdForCommand(parsed.scope, invocation));
20
+ const found = findEntryById(state, id);
21
+ if (!found) {
22
+ return error(`entry ${id} not found in the ${parsed.scope} store`);
23
+ }
24
+ const [kind, entry] = found;
25
+ const metadata = { ...entry.metadata };
26
+ if (sub === "archive") {
27
+ metadata[ARCHIVED_AT_KEY] = new Date().toISOString();
28
+ }
29
+ else {
30
+ delete metadata[ARCHIVED_AT_KEY];
31
+ }
32
+ const archived = sub === "archive";
33
+ const result = engine.apply(parsed.scope, storeIdForCommand(parsed.scope, invocation), {
34
+ summary: `${archived ? "Archive" : "Unarchive"} entry ${kind}:${id}`,
35
+ rationale: "Human-invoked archive/unarchive via the /evolve command.",
36
+ expectedOutcome: `Entry ${archived ? "is hidden from injection (data kept, restorable)" : "is injected again"}.`,
37
+ edits: [{ action: "update", kind, id, title: entry.title, content: entry.content, metadata }],
38
+ }, { scope: parsed.scope });
39
+ return success(renderResult(result));
40
+ }
41
+ /** `/evolve consolidate [apply] [merge]` — deterministic global-store hygiene. */
42
+ export function executeConsolidateCommand(engine, rest) {
43
+ // R3: deterministic global-store hygiene. Report by default; `apply`
44
+ // re-scans fresh state and lands the whole batch as ONE refinement
45
+ // (single snapshot + audit record, fully rollback-able). `merge`
46
+ // (P1 反膨胀) additionally merges conflict-pair content into the
47
+ // surviving original instead of only archiving.
48
+ const apply = rest[0] === "apply";
49
+ const merge = rest.includes("merge");
50
+ const state = engine.load("global", undefined);
51
+ const { candidates, edits } = planConsolidation(state, loadUsage(engine.baseDir), Date.now(), { mergeDuplicates: merge });
52
+ if (candidates.length === 0) {
53
+ return success("global store is already consolidated — no conflict-hinted or stale zero-use entries.");
54
+ }
55
+ const mergeCount = merge ? candidates.filter((candidate) => candidate.mergeInto).length : 0;
56
+ const report = candidates
57
+ .map((candidate, index) => {
58
+ const mergeNote = candidate.mergeInto && merge ? ` → 内容并入 ${candidate.mergeInto.id}` : "";
59
+ return `${index + 1}. [${candidate.kind}:${candidate.id}] ${candidate.title}${mergeNote}\n ${candidate.reason}`;
60
+ })
61
+ .join("\n");
62
+ if (!apply) {
63
+ return success(`consolidation plan — ${candidates.length} archive candidate(s):\n${report}\n(run "/evolve consolidate apply" to archive all of them in one refinement; add "merge" to fold near-duplicate content into the survivors)`);
64
+ }
65
+ const result = engine.apply("global", undefined, {
66
+ summary: `Consolidate global store: archive ${candidates.length} entries${mergeCount > 0 ? `, merge ${mergeCount} into survivors` : ""}`,
67
+ rationale: "Human-invoked batch consolidation via /evolve consolidate apply.",
68
+ expectedOutcome: "Candidate entries are hidden from injection (data kept; restorable via /evolve unarchive). Merged survivors carry the near-duplicate content with a mergedFrom provenance stamp.",
69
+ edits,
70
+ }, { scope: "global" });
71
+ return success(`${report}\n\napplied:\n${renderResult(result)}`);
72
+ }
73
+ /** `/evolve failures` — failure-signature aggregation (D1 observation). */
74
+ export function executeFailuresCommand(engine) {
75
+ const { summary, records } = collectFailureSummary(engine.baseDir);
76
+ const parts = formatFailureSummary(summary).split("\n");
77
+ const recent = records
78
+ .sort((a, b) => (b.timestamp ?? "").localeCompare(a.timestamp ?? ""))
79
+ .slice(0, 10)
80
+ .map((f) => ` [${f.timestamp ?? "(benchmark)"}] ${f.kind} · ${f.source}: ${f.message.slice(0, 140)}`);
81
+ if (recent.length > 0) {
82
+ parts.push("recent 10:");
83
+ parts.push(...recent);
84
+ }
85
+ return success(parts.join("\n"));
86
+ }
87
+ /** `/evolve log [tail N] [session <sessionId>]` — plugin file log inspection. */
88
+ export function executeLogCommand(engine, rest, usage) {
89
+ let tail = 50;
90
+ let sessionFilter;
91
+ for (let i = 0; i < rest.length; i += 1) {
92
+ const token = rest[i] ?? "";
93
+ if (token === "session") {
94
+ sessionFilter = stripAngleBrackets(rest[i + 1] ?? "");
95
+ if (!sessionFilter) {
96
+ return error(`log session requires a session id (e.g. /evolve log session session-abc123).\n${usage}`);
97
+ }
98
+ i += 1;
99
+ }
100
+ else {
101
+ tail = Math.min(Math.max(parsePositiveInt(token, "tail"), 1), 1000);
102
+ }
103
+ }
104
+ const path = pluginLogFilePath(engine.baseDir);
105
+ if (!existsSync(path)) {
106
+ return success(`(no plugin log yet — ${path} is created on the first log message)`);
107
+ }
108
+ const lines = readFileSync(path, "utf8").trimEnd().split("\n").filter((line) => line.length > 0);
109
+ if (lines.length === 0) {
110
+ return success(`(empty plugin log: ${path})`);
111
+ }
112
+ const filtered = sessionFilter ? filterLogBySession(lines, sessionFilter) : lines;
113
+ const shown = filtered.slice(-tail);
114
+ const scopeNote = sessionFilter ? `, ${filtered.length} for session ${sessionFilter}` : "";
115
+ return success(`plugin log ${path} (${lines.length} lines${scopeNote}, showing last ${shown.length}):\n${shown.map(formatLogLine).join("\n")}`);
116
+ }
117
+ /** `/evolve export [global|project] <path>` — backup a store to a JSON file. */
118
+ export function executeExportCommand(engine, invocation, parsed, usage) {
119
+ const path = parsed.rest[0];
120
+ if (!path) {
121
+ return error(`export requires an output path.\n${usage}`);
122
+ }
123
+ const payload = engine.exportStore(parsed.scope, storeIdForCommand(parsed.scope, invocation));
124
+ writeFileSync(path, `${JSON.stringify(payload, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
125
+ return success(`exported ${parsed.scope} store (${Object.values(payload.entries).reduce((n, e) => n + Object.keys(e).length, 0)} entries, ${payload.history.length} refinements) to ${path}`);
126
+ }
127
+ /** `/evolve import [global|project] <path>` — restore a store from an export file. */
128
+ export function executeImportCommand(engine, invocation, parsed, path) {
129
+ if (!path) {
130
+ return error(`import requires an input path.`);
131
+ }
132
+ const payload = JSON.parse(readFileSync(path, "utf8"));
133
+ const summary = engine.importStore(parsed.scope, storeIdForCommand(parsed.scope, invocation), payload);
134
+ return success(`imported ${parsed.scope} store from ${path} (${summary.entries} entries, ${summary.refinements} refinements)`);
135
+ }
136
+ //# sourceMappingURL=maintenance-commands.js.map
@@ -6,17 +6,18 @@
6
6
  * structured proposal tool, and never writes state itself. Every accepted edit
7
7
  * is handed to EvolutionEngine so scope approval, snapshots, versioning,
8
8
  * rollback, and audit remain mandatory.
9
+ *
10
+ * Split (2026-10-03 refactor): the manifest view lives in
11
+ * `memory-manifest.ts`, proposal application in `memory-apply.ts`.
9
12
  */
10
13
  import type { Context } from "@deepseek-ai/cordis";
11
- import type { Agent } from "@deepseek-ai/dsh-agent";
12
14
  import { type ToolSchema } from "@deepseek-ai/dsh-llm";
13
- import { type ScopeApprovalDecision } from "./approval.js";
14
15
  import { type LlmSessionId } from "./llm-text.js";
16
+ import { type MemoryManifestEntry } from "./memory-manifest.js";
15
17
  import { type PrefixCacheOptions } from "./prefix-cache.js";
16
- import { type EvolutionEngine } from "./service.js";
17
18
  import { type TokenUsageTarget } from "./token-usage.js";
18
19
  import { type RecordLanguage } from "./record-language.js";
19
- import type { EntrySource, HarnessEntry, HarnessScope, HarnessState, RefinementEdit, RefinementProposal, RefinementResult } from "./types.js";
20
+ import type { HarnessScope, HarnessState, RefinementEdit, RefinementProposal, RefinementResult } from "./types.js";
20
21
  /** ZCode parity: the extractor may inspect memory and propose at most five times. */
21
22
  export declare const MEMORY_AGENT_MAX_TURNS = 5;
22
23
  /** Bound one proposal so approval text and engine compensation stay reviewable. */
@@ -32,7 +33,6 @@ export declare const MEMORY_AGENT_SYSTEM_PROMPT_BUDGET_CHARS = 3000;
32
33
  export declare const MEMORY_AGENT_TOOL_NAMES: readonly ["memory_search", "memory_propose"];
33
34
  export type MemoryAgentToolName = (typeof MEMORY_AGENT_TOOL_NAMES)[number];
34
35
  /** One frozen memory entry visible to the extractor for the whole run. */
35
- export type MemoryManifestEntry = HarnessEntry;
36
36
  /** A memory-only edit with an explicit persistence target. */
37
37
  export type ScopedMemoryEdit = RefinementEdit & {
38
38
  kind: "memory";
@@ -63,17 +63,10 @@ export interface MemoryApplicationResult {
63
63
  export declare const MEMORY_AGENT_TOOL_SCHEMAS: readonly ToolSchema[];
64
64
  /** Instructions for the dedicated loop; model text is always untrusted. */
65
65
  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.";
66
- /** Build the frozen memory manifest from a merged harness view. */
67
- export declare function buildMemoryManifest(state: HarnessState): MemoryManifestEntry[];
68
- /** Render a bounded manifest index; full bodies remain available through memory_search. */
69
- export declare function formatMemoryManifest(entries: readonly MemoryManifestEntry[], maxChars?: number): string;
70
- /** Rank frozen manifest entries with CJK-aware token overlap and stable tie-breaking. */
71
- export declare function searchMemoryManifest(entries: readonly MemoryManifestEntry[], query: string, maxResults?: number): MemoryManifestEntry[];
72
- /** Inputs for one bounded background memory extraction loop. */
73
- export interface MemoryAgentOptions {
66
+ /** Build the frozen memory manifest from a merged harness view. */ export interface MemoryAgentOptions {
74
67
  provider: string;
75
68
  model: string;
76
- sessionId?: LlmSessionId;
69
+ sessionId?: LlmSessionId | string;
77
70
  manifest: readonly MemoryManifestEntry[];
78
71
  trajectory: string;
79
72
  trajectoryEvents?: readonly unknown[];
@@ -97,31 +90,4 @@ export interface MemoryAgentOptions {
97
90
  export declare function runMemoryAgent(ctx: Context, options: MemoryAgentOptions): Promise<MemoryAgentRun>;
98
91
  /** Strictly parse and policy-check the structured `memory_propose` payload. */
99
92
  export declare function parseMemoryExtractionProposal(value: unknown, manifest: readonly MemoryManifestEntry[]): MemoryExtractionProposal;
100
- export interface ApplyMemoryProposalOptions {
101
- agent: Agent;
102
- baselines: MemoryScopeBaselines;
103
- projectKey?: string;
104
- requireApproval: boolean;
105
- source?: EntrySource;
106
- signal?: AbortSignal;
107
- /** Outer cursor used to scope reusable approval decisions. */
108
- decisionCursor?: string;
109
- /** Prior decisions for the same cursor and exact scoped proposal. */
110
- scopeDecisions?: Readonly<Record<string, ScopeApprovalDecision>>;
111
- /** Persist each decision before a later scope can fail. */
112
- onScopeDecision?: (key: string, decision: ScopeApprovalDecision) => void;
113
- }
114
- /**
115
- * Render the authoritative persistent-scope diff shown to the human. Model
116
- * prose is retained only as an explicitly untrusted summary; actions, targets,
117
- * content previews, memory types, and conflict warnings come from code.
118
- */
119
- export declare function renderMemoryApprovalDetails(proposal: MemoryExtractionProposal, scope: "project" | "global", state: HarnessState, maxChars?: number): string;
120
- /**
121
- * Apply one accepted proposal through the only mutation engine. All persistent
122
- * approvals are resolved before the first write, so an unavailable approval
123
- * boundary cannot leave a partially applied batch. Explicit rejection is a
124
- * no-op for that scope; service/question failures remain retryable errors.
125
- */
126
- export declare function applyMemoryExtractionProposal(ctx: Context, engine: EvolutionEngine, proposal: MemoryExtractionProposal, options: ApplyMemoryProposalOptions): Promise<MemoryApplicationResult>;
127
93
  //# sourceMappingURL=memory-agent.d.ts.map
@@ -1,18 +1,13 @@
1
1
  import { createAssistantMessage, createToolResultMessage, createUserMessage, } from "@deepseek-ai/dsh-llm";
2
- import { requestScopeApproval } from "./approval.js";
3
- import { fingerprintMemoryBatch, isDeclinedRepeat, loadDeclinedMemory, recordDeclinedMemory, } from "./declines.js";
4
- import { streamModelTurn } from "./llm-text.js";
5
2
  import { EVOLVE_MESSAGE_SOURCE } from "./message-source.js";
3
+ import { asLlmSessionId, streamModelTurn } from "./llm-text.js";
4
+ import { formatMemoryManifest, manifestResult, searchMemoryManifest } from "./memory-manifest.js";
5
+ import { validateBlastRadiusScope, validateEdit } from "./validate.js";
6
6
  import { extractJsonObject, parseJsonCandidate } from "./plan.js";
7
7
  import { buildPrefixMessages, detectPlannerRoute, resolvePrefixCache } from "./prefix-cache.js";
8
- import { compactText } from "./render.js";
9
- import { buildConflictNotice, CONFLICT_WARN_SCORE, mostSimilarEntry } from "./promotion.js";
10
- import { EvolutionApplyPostCommitError } from "./service.js";
11
- import { tokenize } from "./search.js";
12
8
  import { createTokenUsageObserver } from "./token-usage.js";
13
9
  import { recordLanguageInstruction, resolveRecordLanguage } from "./record-language.js";
14
- import { isArchived, isMemoryType, MEMORY_TYPE_KEY, slug } from "./types.js";
15
- import { validateBlastRadiusScope, validateEdit } from "./validate.js";
10
+ import { MEMORY_TYPE_KEY, slug } from "./types.js";
16
11
  /** ZCode parity: the extractor may inspect memory and propose at most five times. */
17
12
  export const MEMORY_AGENT_MAX_TURNS = 5;
18
13
  /** Bound one proposal so approval text and engine compensation stay reviewable. */
@@ -103,57 +98,6 @@ Rules:
103
98
  - 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.
104
99
  - If no durable memory is justified, call memory_propose with edits=[].
105
100
  - Always finish by calling memory_propose. Do not return unproposed prose.`;
106
- /** Build the frozen memory manifest from a merged harness view. */
107
- export function buildMemoryManifest(state) {
108
- return Object.values(state.entries.memory)
109
- .map((entry) => ({ ...entry, metadata: { ...entry.metadata }, reference: {}, arguments: {} }))
110
- .sort((a, b) => manifestOrder(a).localeCompare(manifestOrder(b), "en"));
111
- }
112
- /** Render a bounded manifest index; full bodies remain available through memory_search. */
113
- export function formatMemoryManifest(entries, maxChars = 8000) {
114
- if (entries.length === 0)
115
- return "No saved memory entries yet.";
116
- const lines = [];
117
- let used = 0;
118
- for (const entry of entries) {
119
- const type = isMemoryType(entry.metadata[MEMORY_TYPE_KEY]) ? entry.metadata[MEMORY_TYPE_KEY] : "untyped";
120
- const archived = isArchived(entry) ? ", archived" : "";
121
- const line = `- [${entry.scope}:${entry.id}] ${entry.title} (memoryType=${type}, path=${entry.path}${archived}): ${compactText(entry.content, 240)}`;
122
- if (used + line.length + 1 > maxChars)
123
- break;
124
- lines.push(line);
125
- used += line.length + 1;
126
- }
127
- const omitted = entries.length - lines.length;
128
- if (omitted > 0)
129
- lines.push(`- +${omitted} more entries; use memory_search with a narrower query.`);
130
- return lines.join("\n");
131
- }
132
- /** Rank frozen manifest entries with CJK-aware token overlap and stable tie-breaking. */
133
- export function searchMemoryManifest(entries, query, maxResults = 8) {
134
- const queryTokens = new Set(tokenize(query));
135
- if (queryTokens.size === 0)
136
- return [];
137
- return entries
138
- .map((entry) => {
139
- const title = tokenize(entry.title);
140
- const body = tokenize(`${entry.content} ${entry.path} ${String(entry.metadata[MEMORY_TYPE_KEY] ?? "")}`);
141
- let score = 0;
142
- for (const token of queryTokens) {
143
- if (title.includes(token))
144
- score += 2;
145
- if (body.includes(token))
146
- score += 1;
147
- }
148
- if (`${entry.scope}:${entry.id}`.toLowerCase() === query.trim().toLowerCase())
149
- score += 100;
150
- return { entry, score };
151
- })
152
- .filter((hit) => hit.score > 0)
153
- .sort((a, b) => b.score - a.score || manifestOrder(a.entry).localeCompare(manifestOrder(b.entry), "en"))
154
- .slice(0, maxResults)
155
- .map((hit) => hit.entry);
156
- }
157
101
  /**
158
102
  * Run the dedicated loop against a frozen manifest.
159
103
  *
@@ -190,7 +134,7 @@ export async function runMemoryAgent(ctx, options) {
190
134
  const blocks = await streamModelTurn(ctx, {
191
135
  provider: options.provider,
192
136
  model: options.model,
193
- ...(options.sessionId ? { sessionId: options.sessionId } : {}),
137
+ ...(options.sessionId ? { sessionId: asLlmSessionId(options.sessionId) } : {}),
194
138
  system: systemPrompt,
195
139
  messages,
196
140
  tools: MEMORY_AGENT_TOOL_SCHEMAS,
@@ -252,188 +196,6 @@ export function parseMemoryExtractionProposal(value, manifest) {
252
196
  const edits = record["edits"].map((raw, index) => parseScopedMemoryEdit(raw, index, byScopeId));
253
197
  return { summary, rationale, expectedOutcome, edits };
254
198
  }
255
- /**
256
- * Render the authoritative persistent-scope diff shown to the human. Model
257
- * prose is retained only as an explicitly untrusted summary; actions, targets,
258
- * content previews, memory types, and conflict warnings come from code.
259
- */
260
- export function renderMemoryApprovalDetails(proposal, scope, state, maxChars = 6000) {
261
- const edits = proposal.edits.filter((candidate) => candidate.targetScope === scope);
262
- const details = edits.map((edit) => {
263
- const before = edit.id ? state.entries.memory[edit.id] : undefined;
264
- const title = edit.title ?? before?.title ?? "(existing title unchanged)";
265
- const path = edit.path ?? before?.path ?? "(path unchanged)";
266
- const content = edit.content ?? (edit.action === "update" ? "(content unchanged)" : "(no content field)");
267
- const type = edit.metadata?.[MEMORY_TYPE_KEY] ?? before?.metadata[MEMORY_TYPE_KEY] ?? "untyped";
268
- const candidates = Object.values(state.entries.memory).filter((entry) => entry.id !== edit.id);
269
- const hit = edit.action === "create" || edit.action === "update"
270
- ? mostSimilarEntry(candidates, edit.title ?? before?.title ?? "", edit.content ?? before?.content ?? "", CONFLICT_WARN_SCORE)
271
- : undefined;
272
- const conflict = hit ? `; conflict: ${compactText(buildConflictNotice(hit), 120)}` : "";
273
- const targetId = edit.id === undefined ? "(new)" : JSON.stringify(edit.id);
274
- return {
275
- target: `- ${edit.action} ${scope}:${targetId} — ${compactText(title, 80)}`,
276
- detail: ` path=${compactText(path, 60)}; memoryType=${String(type)}; content=${compactText(content, 100)}${conflict}`,
277
- };
278
- });
279
- let titleLimit = 80;
280
- let targets = details.map((detail) => detail.target.replace(/ — .*$/, ` — ${compactText(detail.target.split(" — ")[1] ?? "", titleLimit)}`));
281
- let base = ["edits to approve:", ...targets].join("\n");
282
- while (base.length > maxChars && titleLimit > 20) {
283
- titleLimit = Math.floor(titleLimit / 2);
284
- targets = details.map((detail) => `- ${detail.target.split(" — ")[0]} — ${compactText(detail.target.split(" — ")[1] ?? "", titleLimit)}`);
285
- base = ["edits to approve:", ...targets].join("\n");
286
- }
287
- if (base.length > maxChars)
288
- throw new Error(`memory approval diff exceeds ${maxChars} characters`);
289
- const output = [base];
290
- const summary = `model summary (untrusted): ${compactText(proposal.summary, 180)}`;
291
- if ([...output, summary].join("\n").length <= maxChars)
292
- output.push(summary);
293
- for (const detail of details) {
294
- const line = detail.detail;
295
- if ([...output, line].join("\n").length <= maxChars)
296
- output.push(line);
297
- }
298
- if ([...output, " (additional previews omitted; every target is listed above)"].join("\n").length <= maxChars) {
299
- output.push(" (additional previews omitted; every target is listed above)");
300
- }
301
- return output.join("\n");
302
- }
303
- function memoryScopeDecisionKey(cursor, scope, edits) {
304
- return `${cursor}:${scope}:${JSON.stringify(edits.map(stripTargetScope))}`;
305
- }
306
- class MemoryApplyBatchError extends Error {
307
- result;
308
- scope;
309
- storeId;
310
- constructor(message, result, scope, storeId) {
311
- super(message);
312
- this.result = result;
313
- this.scope = scope;
314
- this.storeId = storeId;
315
- }
316
- }
317
- /**
318
- * Apply one accepted proposal through the only mutation engine. All persistent
319
- * approvals are resolved before the first write, so an unavailable approval
320
- * boundary cannot leave a partially applied batch. Explicit rejection is a
321
- * no-op for that scope; service/question failures remain retryable errors.
322
- */
323
- export async function applyMemoryExtractionProposal(ctx, engine, proposal, options) {
324
- const scopeOrder = ["local", "project", "global"];
325
- const editsByScope = new Map();
326
- for (const scope of scopeOrder) {
327
- const edits = proposal.edits.filter((edit) => edit.targetScope === scope);
328
- if (edits.length > 0)
329
- editsByScope.set(scope, edits);
330
- }
331
- if (editsByScope.has("project") && !options.projectKey) {
332
- throw new Error("memory agent cannot target project scope without a resolved project key");
333
- }
334
- const approvedScopes = new Set();
335
- const declinedScopes = [];
336
- for (const scope of ["project", "global"]) {
337
- if (!editsByScope.has(scope))
338
- continue;
339
- if (!options.requireApproval) {
340
- approvedScopes.add(scope);
341
- continue;
342
- }
343
- const baseline = options.baselines[scope];
344
- if (!baseline)
345
- throw new Error(`memory agent has no ${scope} baseline`);
346
- const scopeEdits = proposal.edits.filter((edit) => edit.targetScope === scope);
347
- const fingerprint = fingerprintMemoryBatch(scope, scopeEdits);
348
- if (isDeclinedRepeat(loadDeclinedMemory(engine.baseDir), scope, fingerprint)) {
349
- ctx.logger("continual-evolve").info(`memory ${scope} proposal repeat-suppressed (declined before, no popup)`);
350
- declinedScopes.push(scope);
351
- continue;
352
- }
353
- const decisionKey = options.decisionCursor === undefined
354
- ? undefined
355
- : memoryScopeDecisionKey(options.decisionCursor, scope, proposal.edits.filter((edit) => edit.targetScope === scope));
356
- const previous = decisionKey === undefined ? undefined : options.scopeDecisions?.[decisionKey];
357
- if (previous === "approved") {
358
- approvedScopes.add(scope);
359
- continue;
360
- }
361
- if (previous === "declined") {
362
- declinedScopes.push(scope);
363
- continue;
364
- }
365
- const approved = await requestScopeApproval(ctx, options.agent, options.signal, scope, renderMemoryApprovalDetails(proposal, scope, baseline));
366
- if (decisionKey !== undefined)
367
- options.onScopeDecision?.(decisionKey, approved);
368
- if (approved === "approved")
369
- approvedScopes.add(scope);
370
- else {
371
- declinedScopes.push(scope);
372
- try {
373
- recordDeclinedMemory(engine.baseDir, scope, scopeEdits, compactText(scopeEdits[0]?.title ?? proposal.summary, 120));
374
- }
375
- catch (error) {
376
- ctx.logger("continual-evolve").warn(`declined-memory ledger write failed: ${error instanceof Error ? error.message : String(error)}`);
377
- }
378
- }
379
- }
380
- const results = [];
381
- const applied = [];
382
- try {
383
- for (const scope of scopeOrder) {
384
- const edits = editsByScope.get(scope);
385
- if (!edits)
386
- continue;
387
- if (scope !== "local" && !approvedScopes.has(scope))
388
- continue;
389
- options.signal?.throwIfAborted();
390
- const storeId = scope === "local" ? options.agent.id : scope === "project" ? options.projectKey : undefined;
391
- const baseline = options.baselines[scope];
392
- if (!baseline)
393
- throw new Error(`memory agent has no ${scope} baseline`);
394
- const result = engine.apply(scope, storeId, {
395
- summary: proposal.summary,
396
- rationale: proposal.rationale,
397
- expectedOutcome: proposal.expectedOutcome,
398
- edits: prepareMemoryEdits(edits, baseline),
399
- }, {
400
- scope,
401
- baselineState: baseline,
402
- ...(options.source ? { source: options.source } : {}),
403
- });
404
- const failed = result.appliedEdits.filter((edit) => !edit.applied);
405
- if (failed.length > 0) {
406
- throw new MemoryApplyBatchError(`${scope} batch ${result.id} left ${failed.length} edit(s) unapplied: ${failed.map((edit) => edit.error ?? "unknown").join("; ")}`, result, scope, storeId);
407
- }
408
- results.push(result);
409
- applied.push({ scope, storeId, result });
410
- }
411
- return { results, declinedScopes };
412
- }
413
- catch (cause) {
414
- const attempted = [...applied];
415
- if (cause instanceof MemoryApplyBatchError && cause.result && cause.scope) {
416
- attempted.push({ scope: cause.scope, storeId: cause.storeId, result: cause.result });
417
- }
418
- else if (cause instanceof EvolutionApplyPostCommitError) {
419
- attempted.push({ scope: cause.scope, storeId: cause.storeId, result: cause.result });
420
- }
421
- const rollbackErrors = [];
422
- for (const item of attempted.reverse()) {
423
- try {
424
- const rollback = engine.rollbackResult(item.scope, item.storeId, item.result);
425
- const failed = rollback.appliedEdits.filter((edit) => !edit.applied);
426
- if (failed.length > 0)
427
- rollbackErrors.push(`${item.scope}:${item.result.id} left ${failed.length} rollback edit(s) unapplied`);
428
- }
429
- catch (rollbackCause) {
430
- rollbackErrors.push(`${item.scope}:${item.result.id} rollback failed: ${rollbackCause instanceof Error ? rollbackCause.message : String(rollbackCause)}`);
431
- }
432
- }
433
- const suffix = rollbackErrors.length > 0 ? `; compensation errors: ${rollbackErrors.join("; ")}` : "; prior memory writes were rolled back";
434
- throw new Error(`memory agent apply failed: ${cause instanceof Error ? cause.message : String(cause)}${suffix}`);
435
- }
436
- }
437
199
  function executeMemoryTool(call, manifest) {
438
200
  if (!MEMORY_AGENT_TOOL_NAMES.includes(call.name)) {
439
201
  return { text: `tool denied: only ${MEMORY_AGENT_TOOL_NAMES.join(" and ")} are available`, isError: true };
@@ -518,44 +280,14 @@ function parseScopedMemoryEdit(raw, index, byScopeId) {
518
280
  throw new Error(`memory edit ${index}: ${validationError}`);
519
281
  return edit;
520
282
  }
521
- function prepareMemoryEdits(edits, baseline) {
522
- return edits.map((edit) => {
523
- const plain = stripTargetScope(edit);
524
- if (plain.action !== "update" || plain.metadata === undefined || plain.id === undefined)
525
- return plain;
526
- const before = baseline.entries.memory[plain.id];
527
- return before ? { ...plain, metadata: { ...before.metadata, ...plain.metadata } } : plain;
528
- });
529
- }
530
- function stripTargetScope(edit) {
531
- const { targetScope: _targetScope, ...plain } = edit;
532
- return plain;
533
- }
534
- function manifestResult(entry) {
535
- return {
536
- scope: entry.scope,
537
- id: entry.id,
538
- title: entry.title,
539
- content: entry.content,
540
- path: entry.path,
541
- memoryType: entry.metadata[MEMORY_TYPE_KEY],
542
- archived: isArchived(entry),
543
- };
544
- }
545
- function manifestOrder(entry) {
546
- const scope = entry.scope === "global" ? "0" : entry.scope === "project" ? "1" : "2";
547
- return `${scope}:${entry.id}:${entry.title}`;
548
- }
549
283
  function textOf(blocks) {
550
284
  return blocks
551
- .filter((block) => block.type === "text")
552
- .map((block) => block.text)
285
+ .map((block) => (block.type === "text" ? block.text : ""))
286
+ .filter((text) => text.length > 0)
553
287
  .join("\n");
554
288
  }
555
289
  function asRecord(value) {
556
- if (typeof value !== "object" || value === null || Array.isArray(value))
557
- return undefined;
558
- return value;
290
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined;
559
291
  }
560
292
  function requiredString(value, field) {
561
293
  if (typeof value !== "string" || value.trim().length === 0)
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Memory proposal application: take the extractor's structured proposal and
3
+ * land it through the governed path — per-scope human approval (unique,
4
+ * explicit, fail-loud), credential screening, conflict notices, engine
5
+ * apply with baseline checks, decline recording, and cross-scope
6
+ * compensation on partial failure.
7
+ */
8
+ import type { Context } from "@deepseek-ai/cordis";
9
+ import type { Agent } from "@deepseek-ai/dsh-agent";
10
+ import type { EntrySource, HarnessState } from "./types.js";
11
+ import { type ScopeApprovalDecision } from "./approval.js";
12
+ import { type EvolutionEngine } from "./service.js";
13
+ import type { MemoryApplicationResult, MemoryExtractionProposal, MemoryScopeBaselines } from "./memory-agent.js";
14
+ import type { RecordLanguagePreference } from "./record-language.js";
15
+ export interface ApplyMemoryProposalOptions {
16
+ agent: Agent;
17
+ baselines: MemoryScopeBaselines;
18
+ projectKey?: string;
19
+ requireApproval: boolean;
20
+ source?: EntrySource;
21
+ signal?: AbortSignal;
22
+ /** Outer cursor used to scope reusable approval decisions. */
23
+ decisionCursor?: string;
24
+ /** Prior decisions for the same cursor and exact scoped proposal. */
25
+ scopeDecisions?: Readonly<Record<string, ScopeApprovalDecision>>;
26
+ /** Persist each decision before a later scope can fail. */
27
+ onScopeDecision?: (key: string, decision: ScopeApprovalDecision) => void;
28
+ /** Plugin `recordLanguage` preference for the approval dialog copy. */
29
+ recordLanguage?: RecordLanguagePreference;
30
+ }
31
+ /**
32
+ * Render the authoritative persistent-scope diff shown to the human. Model
33
+ * prose is retained only as an explicitly untrusted summary; actions, targets,
34
+ * content previews, memory types, and conflict warnings come from code.
35
+ */
36
+ export declare function renderMemoryApprovalDetails(proposal: MemoryExtractionProposal, scope: "project" | "global", state: HarnessState, maxChars?: number): string;
37
+ /**
38
+ * Apply one accepted proposal through the only mutation engine. All persistent
39
+ * approvals are resolved before the first write, so an unavailable approval
40
+ * boundary cannot leave a partially applied batch. Explicit rejection is a
41
+ * no-op for that scope; service/question failures remain retryable errors.
42
+ */
43
+ export declare function applyMemoryExtractionProposal(ctx: Context, engine: EvolutionEngine, proposal: MemoryExtractionProposal, options: ApplyMemoryProposalOptions): Promise<MemoryApplicationResult>;
44
+ //# sourceMappingURL=memory-apply.d.ts.map