dsh-continual-evolve 0.7.6 → 0.8.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/lib/recall.js ADDED
@@ -0,0 +1,142 @@
1
+ import { buildRelevanceIndex, relevanceScore } from "./search.js";
2
+ import { CONFLICT_HINT_KEY } from "./types.js";
3
+ import { isArchived, isMemoryType, ARCHIVED_AT_KEY, MEMORY_TYPE_KEY, SOURCE_SEQS_KEY, SOURCE_SESSION_KEY } from "./types.js";
4
+ /** Memory recall types (the `memoryType` taxonomy, not entry kinds). */
5
+ export const RECALL_MEMORY_TYPES = ["user", "feedback", "project", "reference"];
6
+ /** Entry kinds recall can filter on; memory is the default. */
7
+ const RECALL_KINDS = ["prompt", "memory", "skill", "subagent"];
8
+ /** Default and maximum hits returned per recall call. */
9
+ export const DEFAULT_RECALL_LIMIT = 10;
10
+ export const MAX_RECALL_LIMIT = 50;
11
+ /**
12
+ * Search the harness stores with mechanical filters and BM25 ranking.
13
+ *
14
+ * @param engine The evolution engine (read-only use).
15
+ * @param context Store identities for the session-scoped stores.
16
+ * @param filters Query/kind/scope/type/limit/archive filters.
17
+ * @returns Ranked hits plus a note for every scope that could not be read.
18
+ * @throws On an unknown kind, scope, or memory type — fail loud instead of
19
+ * silently narrowing the search.
20
+ */
21
+ export function recallMemories(engine, context, filters = {}) {
22
+ const kinds = filters.kinds ?? ["memory"];
23
+ for (const kind of kinds) {
24
+ if (!RECALL_KINDS.includes(kind)) {
25
+ throw new Error(`evolve_recall: unknown kind "${kind}" (expected one of ${RECALL_KINDS.join(", ")})`);
26
+ }
27
+ }
28
+ const scopes = filters.scopes ?? ["local", "project", "global"];
29
+ for (const scope of scopes) {
30
+ if (scope !== "local" && scope !== "project" && scope !== "global") {
31
+ throw new Error(`evolve_recall: unknown scope "${scope}" (expected local, project, or global)`);
32
+ }
33
+ }
34
+ const memoryTypes = filters.memoryTypes ?? [];
35
+ for (const memoryType of memoryTypes) {
36
+ if (!RECALL_MEMORY_TYPES.includes(memoryType)) {
37
+ throw new Error(`evolve_recall: unknown memoryType "${memoryType}" (expected one of ${RECALL_MEMORY_TYPES.join(", ")})`);
38
+ }
39
+ }
40
+ const limit = typeof filters.limit === "number" && Number.isFinite(filters.limit) && filters.limit > 0
41
+ ? Math.min(Math.floor(filters.limit), MAX_RECALL_LIMIT)
42
+ : DEFAULT_RECALL_LIMIT;
43
+ const query = filters.query?.trim() ?? "";
44
+ const candidates = [];
45
+ const notes = [];
46
+ for (const scope of scopes) {
47
+ const storeId = scope === "local" ? context.sessionId : scope === "project" ? context.projectKey : undefined;
48
+ if (scope !== "global" && storeId === undefined) {
49
+ notes.push(`${scope} store skipped: ${scope === "local" ? "no live session id" : "no project key for this session"}`);
50
+ continue;
51
+ }
52
+ let state;
53
+ try {
54
+ state = engine.load(scope, storeId);
55
+ }
56
+ catch (cause) {
57
+ notes.push(`${scope} store unreadable: ${cause instanceof Error ? cause.message : String(cause)}`);
58
+ continue;
59
+ }
60
+ for (const kind of kinds) {
61
+ for (const entry of Object.values(state.entries[kind])) {
62
+ if (!filters.includeArchived && isArchived(entry))
63
+ continue;
64
+ if (kind === "memory" && memoryTypes.length > 0) {
65
+ const type = entry.metadata[MEMORY_TYPE_KEY];
66
+ if (typeof type !== "string" || !memoryTypes.includes(type))
67
+ continue;
68
+ }
69
+ candidates.push(entry);
70
+ }
71
+ }
72
+ }
73
+ const ranked = query.length > 0 ? rankByRelevance(candidates, query) : [...candidates].sort((a, b) => (a.updated_at < b.updated_at ? 1 : a.updated_at > b.updated_at ? -1 : 0));
74
+ return { hits: ranked.slice(0, limit).map(toRecallHit), notes, totalCandidates: candidates.length };
75
+ }
76
+ function rankByRelevance(candidates, query) {
77
+ const index = buildRelevanceIndex(candidates);
78
+ const normalized = query.trim().toLowerCase();
79
+ return candidates
80
+ .map((entry) => {
81
+ let score = relevanceScore(index, entry, query);
82
+ // Exact scope:id addressing wins over fuzzy relevance (the
83
+ // memory-agent manifest search precedent).
84
+ if (`${entry.scope}:${entry.id}`.toLowerCase() === normalized)
85
+ score += 1000;
86
+ return { entry, score };
87
+ })
88
+ .filter((hit) => hit.score > 0)
89
+ .sort((a, b) => b.score - a.score || (a.entry.updated_at < b.entry.updated_at ? 1 : -1))
90
+ .map((hit) => hit.entry);
91
+ }
92
+ function toRecallHit(entry) {
93
+ const memoryType = entry.kind === "memory" && isMemoryType(entry.metadata[MEMORY_TYPE_KEY])
94
+ ? String(entry.metadata[MEMORY_TYPE_KEY])
95
+ : undefined;
96
+ const staleHint = typeof entry.metadata[CONFLICT_HINT_KEY] === "string" && entry.metadata[CONFLICT_HINT_KEY].length > 0
97
+ ? String(entry.metadata[CONFLICT_HINT_KEY])
98
+ : undefined;
99
+ const sourceSession = typeof entry.metadata[SOURCE_SESSION_KEY] === "string" ? String(entry.metadata[SOURCE_SESSION_KEY]) : undefined;
100
+ const rawSeqs = entry.metadata[SOURCE_SEQS_KEY];
101
+ const sourceSeqs = Array.isArray(rawSeqs) && rawSeqs.every((seq) => typeof seq === "number") ? rawSeqs : undefined;
102
+ return {
103
+ scope: entry.scope,
104
+ kind: entry.kind,
105
+ id: entry.id,
106
+ title: entry.title,
107
+ content: entry.content,
108
+ path: entry.path,
109
+ version: entry.version,
110
+ createdAt: entry.created_at,
111
+ updatedAt: entry.updated_at,
112
+ archived: isArchived(entry),
113
+ ...(memoryType !== undefined ? { memoryType } : {}),
114
+ ...(staleHint !== undefined ? { staleHint } : {}),
115
+ ...(sourceSession !== undefined ? { sourceSession } : {}),
116
+ ...(sourceSeqs !== undefined ? { sourceSeqs } : {}),
117
+ };
118
+ }
119
+ /**
120
+ * Render recall hits for a model or human reader: full content, version,
121
+ * source, and staleness on every hit; skipped scopes are always shown so
122
+ * "no results" can never hide an unread store.
123
+ */
124
+ export function formatRecallResult(result, query) {
125
+ const lines = [`recall: ${result.hits.length} hit(s)${query?.trim() ? ` for "${query.trim()}"` : ""} (${result.totalCandidates} candidate(s))`];
126
+ for (const hit of result.hits) {
127
+ const type = hit.memoryType !== undefined ? ` · type=${hit.memoryType}` : "";
128
+ const archived = hit.archived ? " · archived" : "";
129
+ const stale = hit.staleHint !== undefined ? ` · conflict-hint=${hit.staleHint}` : "";
130
+ const source = hit.sourceSession !== undefined ? ` · src=${hit.sourceSession}${hit.sourceSeqs ? `:${hit.sourceSeqs.join(",")}` : ""}` : "";
131
+ lines.push(`- [${hit.scope}:${hit.kind}:${hit.id}] ${hit.title} (v${hit.version}${type}${archived}${stale}${source})`);
132
+ lines.push(` ${hit.content}`);
133
+ }
134
+ for (const note of result.notes) {
135
+ lines.push(`note: ${note}`);
136
+ }
137
+ if (result.hits.length === 0) {
138
+ lines.push(`(${ARCHIVED_AT_KEY} entries are hidden unless includeArchived is set)`);
139
+ }
140
+ return lines.join("\n");
141
+ }
142
+ //# sourceMappingURL=recall.js.map
package/lib/service.js CHANGED
@@ -4,6 +4,7 @@ import { randomUUID } from "node:crypto";
4
4
  import { rollbackProposal } from "./rollback.js";
5
5
  import { loadHarnessState, saveHarnessState } from "./state.js";
6
6
  import { appendResult, loadResults, pruneJsonlFile, pruneSnapshots, resolveHistoryRetention, snapshotBefore, storePaths } from "./store.js";
7
+ import { materializeMemoryProjection } from "./projection.js";
7
8
  import { CONFLICT_BLOCK_SCORE, CONFLICT_WARN_SCORE, buildConflictNotice, mostSimilarEntry, secretLeakReason } from "./promotion.js";
8
9
  /** A post-commit apply failure that still carries the durable applied result. */
9
10
  export class EvolutionApplyPostCommitError extends Error {
@@ -137,6 +138,19 @@ export function createEvolutionEngine(baseDir, hooks = {}, opts = {}) {
137
138
  catch {
138
139
  // ignored — the next apply retries
139
140
  }
141
+ // Readable Markdown projection (P1): rewritten from the post-apply
142
+ // state whenever a memory edit landed, so rollback and archive stay
143
+ // in sync through this same apply path. The model has no writer for
144
+ // these files — only this engine entry point materializes them.
145
+ if (result.appliedEdits.some((edit) => edit.applied && edit.kind === "memory")) {
146
+ try {
147
+ materializeMemoryProjection(paths.stateDir, state);
148
+ }
149
+ catch {
150
+ // ignored — the projection is derived (JSON stays the source
151
+ // of truth) and the next memory apply retries
152
+ }
153
+ }
140
154
  try {
141
155
  hooks.onApplied?.(result);
142
156
  }
package/lib/tool.js CHANGED
@@ -5,6 +5,7 @@ import { formatHarnessStateForPrompt } from "./render.js";
5
5
  import { requireGlobalApproval } from "./approval.js";
6
6
  import { CONFLICT_WARN_SCORE, buildConflictNotice, mostSimilarEntry } from "./promotion.js";
7
7
  import { entrySourceOf } from "./source.js";
8
+ import { formatRecallResult, recallMemories } from "./recall.js";
8
9
  import { getUsageCount, loadUsage } from "./usage.js";
9
10
  import { buildEvolveCompleteEvent, emitEvolveComplete } from "./evolve-event.js";
10
11
  import { DEFAULT_REVIEWS_RETAIN } from "./store.js";
@@ -115,6 +116,45 @@ export function registerEvolveTools(ctx, engine, opts) {
115
116
  return textResult(text);
116
117
  },
117
118
  }));
119
+ ctx.tools.register(defineTool({
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.",
122
+ parameters: {
123
+ query: { type: "string", description: "Free-text relevance query. Empty means most-recently-updated first." },
124
+ kinds: {
125
+ type: "array",
126
+ items: { type: "string" },
127
+ description: "Entry kinds to search (default memory only).",
128
+ },
129
+ scopes: {
130
+ type: "array",
131
+ items: { type: "string" },
132
+ description: "Stores to search (default local, project, and global).",
133
+ },
134
+ memoryTypes: {
135
+ type: "array",
136
+ items: { type: "string" },
137
+ description: "Memory-type filter: user, feedback, project, reference (memory entries only).",
138
+ },
139
+ limit: { type: "number", description: "Maximum hits (default 10, at most 50)." },
140
+ includeArchived: { type: "boolean", description: "Include archived entries (default false)." },
141
+ },
142
+ output: {
143
+ schema: { type: "object", additionalProperties: false, properties: { text: { type: "string", required: true } } },
144
+ render: (_args, value) => [{ type: "text", text: value.text ?? "" }],
145
+ },
146
+ execute: async (args, exec) => {
147
+ const result = recallMemories(engine, { sessionId: sessionIdOf(exec), projectKey: projectKeyOf(exec.agent) ?? undefined }, {
148
+ ...(typeof args.query === "string" ? { query: args.query } : {}),
149
+ ...(Array.isArray(args.kinds) ? { kinds: args.kinds } : {}),
150
+ ...(Array.isArray(args.scopes) ? { scopes: args.scopes } : {}),
151
+ ...(Array.isArray(args.memoryTypes) ? { memoryTypes: args.memoryTypes } : {}),
152
+ ...(typeof args.limit === "number" ? { limit: args.limit } : {}),
153
+ ...(typeof args.includeArchived === "boolean" ? { includeArchived: args.includeArchived } : {}),
154
+ });
155
+ return textResult(formatRecallResult(result, typeof args.query === "string" ? args.query : undefined));
156
+ },
157
+ }));
118
158
  ctx.tools.register(defineTool({
119
159
  name: "evolve_add",
120
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.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-continual-evolve",
3
- "version": "0.7.6",
3
+ "version": "0.8.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": {