dsh-continual-evolve 0.14.2 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -23
- package/README.zh.md +35 -23
- package/client/client.js +32 -43
- package/lib/card-routes.d.ts +3 -3
- package/lib/card-routes.js +38 -50
- package/lib/extraction-prompt.d.ts +52 -0
- package/lib/extraction-prompt.js +139 -0
- package/lib/extraction-surface.d.ts +66 -0
- package/lib/extraction-surface.js +186 -0
- package/lib/extraction.d.ts +56 -0
- package/lib/extraction.js +280 -0
- package/lib/import-md.d.ts +15 -0
- package/lib/import-md.js +140 -0
- package/lib/index.d.ts +56 -27
- package/lib/index.js +120 -65
- package/lib/memory-guide.d.ts +22 -14
- package/lib/memory-guide.js +33 -76
- package/lib/memory-rules.d.ts +56 -0
- package/lib/memory-rules.js +152 -0
- package/lib/memory-section.d.ts +26 -26
- package/lib/memory-section.js +48 -82
- package/lib/memory-snapshot.d.ts +43 -59
- package/lib/memory-snapshot.js +101 -141
- package/lib/memory-tools.d.ts +34 -0
- package/lib/memory-tools.js +142 -0
- package/lib/store-apply.d.ts +46 -0
- package/lib/store-apply.js +177 -0
- package/lib/store.d.ts +99 -0
- package/lib/store.js +241 -0
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +8 -6
- package/lib/known-workspaces.d.ts +0 -22
- package/lib/known-workspaces.js +0 -42
- package/lib/workspace-catalog.d.ts +0 -30
- package/lib/workspace-catalog.js +0 -149
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { boundaryCursorOf, evaluateSlice, eventsAfterCursor, isInternalAgent, seqOfCursor, serializeIncrement } from "./extraction-surface.js";
|
|
2
|
+
import { EXTRACTION_SYSTEM_PROMPT, extractionPrompt, manifestLineOf, parseExtractionAnswer } from "./extraction-prompt.js";
|
|
3
|
+
import { cwdOf } from "./memory-section.js";
|
|
4
|
+
/**
|
|
5
|
+
* Run one extraction pass for a target. Never throws: every terminal path
|
|
6
|
+
* is either a landed application, a ledgered skip, or a ledgered failure.
|
|
7
|
+
*/
|
|
8
|
+
export async function runExtraction(deps, target, opts) {
|
|
9
|
+
const { store } = deps;
|
|
10
|
+
const ledgerBase = {
|
|
11
|
+
sessionId: target.sessionId,
|
|
12
|
+
workspaceId: target.workspaceId,
|
|
13
|
+
trigger: opts.trigger,
|
|
14
|
+
};
|
|
15
|
+
if (target.internalAgent) {
|
|
16
|
+
store.logSkipped({ ...ledgerBase, skipReason: "internal-agent", status: "skipped" });
|
|
17
|
+
return { status: "skipped", skipReason: "internal-agent" };
|
|
18
|
+
}
|
|
19
|
+
if (target.workspaceId === undefined) {
|
|
20
|
+
store.logSkipped({ ...ledgerBase, skipReason: "no-workspace", status: "skipped" });
|
|
21
|
+
return { status: "skipped", skipReason: "no-workspace" };
|
|
22
|
+
}
|
|
23
|
+
if (deps.surface === undefined) {
|
|
24
|
+
store.logSkipped({ ...ledgerBase, skipReason: "no-surface", status: "skipped" });
|
|
25
|
+
return { status: "skipped", skipReason: "no-surface" };
|
|
26
|
+
}
|
|
27
|
+
if (target.provider === undefined || target.model === undefined || deps.llm === undefined) {
|
|
28
|
+
store.logSkipped({ ...ledgerBase, skipReason: "no-route", status: "skipped" });
|
|
29
|
+
return { status: "skipped", skipReason: "no-route" };
|
|
30
|
+
}
|
|
31
|
+
const cursor = store.cursor(target.workspaceId, target.sessionId);
|
|
32
|
+
let all;
|
|
33
|
+
try {
|
|
34
|
+
const snapshot = await deps.surface.readSurface(target.sessionId);
|
|
35
|
+
all = Array.isArray(snapshot.events) ? snapshot.events : [];
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
store.logSkipped({ ...ledgerBase, skipReason: "surface-read-failed", status: "failed" });
|
|
39
|
+
return { status: "failed", skipReason: `surface read failed: ${error instanceof Error ? error.message : String(error)}` };
|
|
40
|
+
}
|
|
41
|
+
const events = eventsAfterCursor(all, cursor === undefined ? undefined : `seq:${cursor}`);
|
|
42
|
+
const slice = evaluateSlice({ events, internalAgent: false });
|
|
43
|
+
if (!slice.eligible) {
|
|
44
|
+
store.logSkipped({ ...ledgerBase, skipReason: slice.skipReason, status: "skipped" });
|
|
45
|
+
return { status: "skipped", skipReason: slice.skipReason };
|
|
46
|
+
}
|
|
47
|
+
const boundary = boundaryCursorOf(all);
|
|
48
|
+
const boundarySeq = seqOfCursor(boundary);
|
|
49
|
+
const workspaceId = target.workspaceId;
|
|
50
|
+
const advanceCursor = () => {
|
|
51
|
+
if (boundarySeq !== undefined) {
|
|
52
|
+
store.setCursor(workspaceId, target.sessionId, boundarySeq);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const increment = serializeIncrement(slice.events, opts.incrementMaxChars);
|
|
56
|
+
const manifest = store.list(target.workspaceId).slice(0, 50).map(manifestLineOf);
|
|
57
|
+
const candidates = candidateRecords(store, target.workspaceId, slice.userText);
|
|
58
|
+
const prompt = extractionPrompt({
|
|
59
|
+
increment,
|
|
60
|
+
manifest,
|
|
61
|
+
candidates: candidates.map(formatCandidate),
|
|
62
|
+
userSeqs: slice.userSeqs,
|
|
63
|
+
});
|
|
64
|
+
const started = Date.now();
|
|
65
|
+
let text;
|
|
66
|
+
let usage;
|
|
67
|
+
try {
|
|
68
|
+
const answer = await streamOnce(deps.llm, target, prompt);
|
|
69
|
+
text = answer.text;
|
|
70
|
+
usage = answer.usage;
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
store.logSkipped({ ...ledgerBase, skipReason: "llm-failed", status: "failed", durationMs: Date.now() - started });
|
|
74
|
+
return { status: "failed", skipReason: `llm call failed: ${error instanceof Error ? error.message : String(error)}` };
|
|
75
|
+
}
|
|
76
|
+
const parsed = parseExtractionAnswer(text);
|
|
77
|
+
if (parsed.decision === "error") {
|
|
78
|
+
// A malformed answer is a failed run: the increment stays unconsumed
|
|
79
|
+
// and the next trigger retries it.
|
|
80
|
+
store.logSkipped({
|
|
81
|
+
...ledgerBase,
|
|
82
|
+
skipReason: parsed.reason,
|
|
83
|
+
status: "failed",
|
|
84
|
+
model: target.model,
|
|
85
|
+
usage,
|
|
86
|
+
durationMs: Date.now() - started,
|
|
87
|
+
});
|
|
88
|
+
return { status: "failed", skipReason: parsed.reason };
|
|
89
|
+
}
|
|
90
|
+
if (parsed.decision === "skip") {
|
|
91
|
+
// A real model decision (even "nothing worth keeping") consumed the
|
|
92
|
+
// increment — advancing the cursor here is what keeps retries honest.
|
|
93
|
+
advanceCursor();
|
|
94
|
+
store.logSkipped({
|
|
95
|
+
...ledgerBase,
|
|
96
|
+
skipReason: `model-skip: ${parsed.reason}`,
|
|
97
|
+
status: "skipped",
|
|
98
|
+
model: target.model,
|
|
99
|
+
usage,
|
|
100
|
+
durationMs: Date.now() - started,
|
|
101
|
+
});
|
|
102
|
+
return { status: "skipped", skipReason: parsed.reason, cursor: boundarySeq };
|
|
103
|
+
}
|
|
104
|
+
const outcomes = store.applyProposals({
|
|
105
|
+
workspaceId: target.workspaceId,
|
|
106
|
+
trigger: opts.trigger,
|
|
107
|
+
sessionId: target.sessionId,
|
|
108
|
+
runId: `extract:${target.sessionId}:${boundarySeq ?? "tail"}`,
|
|
109
|
+
model: target.model,
|
|
110
|
+
usage,
|
|
111
|
+
decision: parsed.reason,
|
|
112
|
+
}, parsed.proposals);
|
|
113
|
+
if (!outcomes.every((outcome) => outcome.ok)) {
|
|
114
|
+
return { status: "rejected", skipReason: outcomes.find((outcome) => !outcome.ok)?.reason };
|
|
115
|
+
}
|
|
116
|
+
advanceCursor();
|
|
117
|
+
// Hygiene rides on every applied run: orphan FTS rows dropped, secret
|
|
118
|
+
// leakage quarantined — never a separate scheduler.
|
|
119
|
+
const patrol = store.patrol();
|
|
120
|
+
void patrol;
|
|
121
|
+
return { status: "applied", applied: outcomes.filter((outcome) => outcome.ok).length, cursor: boundarySeq };
|
|
122
|
+
}
|
|
123
|
+
/** Similar existing memories (full bodies) for the model to update against. */
|
|
124
|
+
function candidateRecords(store, workspaceId, userText) {
|
|
125
|
+
const query = userText.split("\n")[0]?.slice(0, 100).trim() ?? "";
|
|
126
|
+
const hits = query.length >= 3 ? store.search(workspaceId, query, 8) : [];
|
|
127
|
+
return hits.length > 0 ? hits : store.list(workspaceId).slice(0, 8);
|
|
128
|
+
}
|
|
129
|
+
function formatCandidate(record) {
|
|
130
|
+
return [`# 候选 id=${record.id}(type=${record.type})`, `title=${record.title}`, `钩子=${record.description}`, "全文:", record.body].join("\n");
|
|
131
|
+
}
|
|
132
|
+
/** One streaming text call with the host session identity forwarded. */
|
|
133
|
+
async function streamOnce(llm, target, prompt) {
|
|
134
|
+
let text = "";
|
|
135
|
+
let usage;
|
|
136
|
+
let finish;
|
|
137
|
+
const options = {
|
|
138
|
+
provider: target.provider,
|
|
139
|
+
model: target.model,
|
|
140
|
+
messages: [{ role: "user", content: [{ type: "text", text: prompt }] }],
|
|
141
|
+
system: EXTRACTION_SYSTEM_PROMPT,
|
|
142
|
+
maxTokens: 4096,
|
|
143
|
+
// MissingSessionID pitfall: direct calls must carry the host session.
|
|
144
|
+
sessionId: target.sessionId,
|
|
145
|
+
};
|
|
146
|
+
for await (const chunk of llm.stream(options)) {
|
|
147
|
+
const piece = chunk;
|
|
148
|
+
if (piece?.type === "text-delta" && typeof piece.text === "string") {
|
|
149
|
+
text += piece.text;
|
|
150
|
+
}
|
|
151
|
+
else if (piece?.type === "usage") {
|
|
152
|
+
usage = piece.usage;
|
|
153
|
+
}
|
|
154
|
+
else if (piece?.type === "finish") {
|
|
155
|
+
finish = piece.reason;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (finish !== undefined && finish.kind !== "stop") {
|
|
159
|
+
throw new Error(`stream finished with ${finish.kind ?? "unknown"}`);
|
|
160
|
+
}
|
|
161
|
+
if (text.trim().length === 0) {
|
|
162
|
+
throw new Error("empty model output");
|
|
163
|
+
}
|
|
164
|
+
return { text, usage };
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Wire the extraction triggers onto the host context. Returns a disposer
|
|
168
|
+
* that removes the listeners. The scheduler is the only background
|
|
169
|
+
* automation the plugin runs, and it is bounded by design: one boundary and
|
|
170
|
+
* one in-flight run per session.
|
|
171
|
+
*/
|
|
172
|
+
export function createExtractionScheduler(host, deps) {
|
|
173
|
+
const listeners = [];
|
|
174
|
+
const slots = new Map();
|
|
175
|
+
const slotOf = (sessionId) => {
|
|
176
|
+
let slot = slots.get(sessionId);
|
|
177
|
+
if (slot === undefined) {
|
|
178
|
+
slot = { pending: null, running: false };
|
|
179
|
+
slots.set(sessionId, slot);
|
|
180
|
+
}
|
|
181
|
+
return slot;
|
|
182
|
+
};
|
|
183
|
+
const targetOf = (agent) => {
|
|
184
|
+
const id = agent?.id;
|
|
185
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
const route = agent?.options;
|
|
189
|
+
return {
|
|
190
|
+
agent,
|
|
191
|
+
sessionId: id,
|
|
192
|
+
provider: typeof route?.provider === "string" ? route.provider : undefined,
|
|
193
|
+
model: typeof route?.model === "string" ? route.model : undefined,
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
const drain = (sessionId, trigger) => {
|
|
197
|
+
const slot = slotOf(sessionId);
|
|
198
|
+
if (slot.running || slot.pending === null) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const target = slot.pending;
|
|
202
|
+
slot.pending = null;
|
|
203
|
+
slot.running = true;
|
|
204
|
+
void runExtraction(deps, {
|
|
205
|
+
workspaceId: cwdOf(target.agent),
|
|
206
|
+
sessionId: target.sessionId,
|
|
207
|
+
internalAgent: isInternalAgent(target.agent),
|
|
208
|
+
provider: target.provider,
|
|
209
|
+
model: target.model,
|
|
210
|
+
}, { trigger }).catch((error) => {
|
|
211
|
+
host.logger("continual-evolve").warn(`extraction run failed unexpectedly: ${error instanceof Error ? error.message : String(error)}`);
|
|
212
|
+
}).finally(() => {
|
|
213
|
+
slot.running = false;
|
|
214
|
+
if (slot.pending !== null) {
|
|
215
|
+
// A turn landed mid-run: the newest boundary is the one to run.
|
|
216
|
+
drain(sessionId, "turn-debounce");
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
};
|
|
220
|
+
/**
|
|
221
|
+
* Schedule one session's boundary. Internal agents (subagents and DSH
|
|
222
|
+
* internal agents) are filtered at this entry rather than inside the run:
|
|
223
|
+
* they are not memory sources, so they never occupy a slot, never drain
|
|
224
|
+
* another session's boundary, and leave no ledger rows behind.
|
|
225
|
+
*/
|
|
226
|
+
const schedule = (agent) => {
|
|
227
|
+
if (isInternalAgent(agent)) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const target = targetOf(agent);
|
|
231
|
+
if (target === null) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
slotOf(target.sessionId).pending = target;
|
|
235
|
+
drain(target.sessionId, "turn-debounce");
|
|
236
|
+
};
|
|
237
|
+
listeners.push(host.on("agent/turn-stopping", (payload) => {
|
|
238
|
+
if (payload.agent === undefined) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
schedule(payload.agent);
|
|
242
|
+
}));
|
|
243
|
+
listeners.push(host.on("session/event", (session, event) => {
|
|
244
|
+
const id = session?.id;
|
|
245
|
+
if (event?.type !== "compaction/start" || typeof id !== "string") {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const slot = slots.get(id);
|
|
249
|
+
if (slot === undefined || slot.pending === null) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
drain(id, "compaction");
|
|
253
|
+
}));
|
|
254
|
+
listeners.push(host.on("agent/disposed", (payload) => {
|
|
255
|
+
const id = payload.agent?.id;
|
|
256
|
+
if (typeof id !== "string") {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const slot = slots.get(id);
|
|
260
|
+
if (slot === undefined) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (slot.pending !== null) {
|
|
264
|
+
drain(id, "close-drain");
|
|
265
|
+
}
|
|
266
|
+
// The session is gone: drop its slot. A run already in flight holds
|
|
267
|
+
// its own reference and simply finds nothing left to drain.
|
|
268
|
+
slots.delete(id);
|
|
269
|
+
}));
|
|
270
|
+
host.logger("continual-evolve").info("extraction scheduler armed (per-session slots, turn-level, single-flight per session)");
|
|
271
|
+
return () => {
|
|
272
|
+
slots.clear();
|
|
273
|
+
for (const listener of listeners) {
|
|
274
|
+
if (typeof listener === "function") {
|
|
275
|
+
listener();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
//# sourceMappingURL=extraction.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type MemoryStore } from "./store.js";
|
|
2
|
+
/** What one workspace's migration did. */
|
|
3
|
+
export interface MdImportResult {
|
|
4
|
+
workspaceId: string;
|
|
5
|
+
/** Records imported (existing ids from a partial earlier run don't count). */
|
|
6
|
+
imported: number;
|
|
7
|
+
/** Records skipped by a hard gate (reasons in `errors`). */
|
|
8
|
+
skipped: number;
|
|
9
|
+
/** The renamed-aside directory, when the rename succeeded. */
|
|
10
|
+
archiveDir: string | null;
|
|
11
|
+
errors: string[];
|
|
12
|
+
}
|
|
13
|
+
/** Run the migration for one workspace root. Never throws past its result. */
|
|
14
|
+
export declare function importWorkspaceMd(store: MemoryStore, cwd: string): MdImportResult;
|
|
15
|
+
//# sourceMappingURL=import-md.d.ts.map
|
package/lib/import-md.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-time migration from the MD-era store: `<workspace>/.evolve/memory/`
|
|
3
|
+
* files are imported into the central database under the workspace's own
|
|
4
|
+
* partition, then the directory is renamed aside (`memory-imported-<date>/`)
|
|
5
|
+
* — never deleted. Runs lazily per workspace on first contact (injection
|
|
6
|
+
* assembly or card read); the `md-imported` state flag keeps it idempotent
|
|
7
|
+
* even when the rename fails.
|
|
8
|
+
*
|
|
9
|
+
* Import is lossless by design: unknown types map to `reference`, missing
|
|
10
|
+
* hooks are synthesized from the body, and only the hard gates (secret
|
|
11
|
+
* screen) can skip a file — skipped files stay in the renamed directory for
|
|
12
|
+
* manual salvage.
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync, readdirSync, readFileSync, renameSync, statSync } from "node:fs";
|
|
15
|
+
import { basename, join } from "node:path";
|
|
16
|
+
import { slugifyId } from "./memory-rules.js";
|
|
17
|
+
import { MD_IMPORT_STATE_KEY } from "./store.js";
|
|
18
|
+
/** The MD-era index file (imported rows come from the other .md files). */
|
|
19
|
+
const MD_INDEX_FILE = "MEMORY.md";
|
|
20
|
+
/** Run the migration for one workspace root. Never throws past its result. */
|
|
21
|
+
export function importWorkspaceMd(store, cwd) {
|
|
22
|
+
const workspaceId = cwd;
|
|
23
|
+
const absent = { workspaceId, imported: 0, skipped: 0, archiveDir: null, errors: [] };
|
|
24
|
+
if (store.state(workspaceId, MD_IMPORT_STATE_KEY) !== undefined) {
|
|
25
|
+
return absent;
|
|
26
|
+
}
|
|
27
|
+
const memoryDir = join(cwd, ".evolve", "memory");
|
|
28
|
+
if (!existsSync(memoryDir) || !statSync(memoryDir).isDirectory()) {
|
|
29
|
+
store.setState(workspaceId, MD_IMPORT_STATE_KEY, new Date().toISOString());
|
|
30
|
+
return absent;
|
|
31
|
+
}
|
|
32
|
+
const result = importMemoryDir(store, workspaceId, memoryDir);
|
|
33
|
+
const archiveDir = renameAside(memoryDir);
|
|
34
|
+
if (archiveDir !== null) {
|
|
35
|
+
result.archiveDir = archiveDir;
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
result.errors.push("the old directory could not be renamed; import flag set anyway (imports are idempotent)");
|
|
39
|
+
}
|
|
40
|
+
store.setState(workspaceId, MD_IMPORT_STATE_KEY, new Date().toISOString());
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
/** Import every non-index .md file of an MD-era store directory. */
|
|
44
|
+
function importMemoryDir(store, workspaceId, memoryDir) {
|
|
45
|
+
const result = { workspaceId, imported: 0, skipped: 0, archiveDir: null, errors: [] };
|
|
46
|
+
const files = readdirSync(memoryDir)
|
|
47
|
+
.filter((file) => file.endsWith(".md") && file !== MD_INDEX_FILE)
|
|
48
|
+
.sort();
|
|
49
|
+
for (const file of files) {
|
|
50
|
+
try {
|
|
51
|
+
const parsed = parseMdMemory(readFileSync(join(memoryDir, file), "utf8"), basename(file, ".md"));
|
|
52
|
+
if (store.get(workspaceId, parsed.id) !== undefined) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const outcomes = store.applyProposals({ workspaceId, trigger: "import", decision: `md migration: ${file}` }, [
|
|
56
|
+
{
|
|
57
|
+
action: "create",
|
|
58
|
+
id: parsed.id,
|
|
59
|
+
type: parsed.type,
|
|
60
|
+
title: parsed.title,
|
|
61
|
+
description: parsed.description,
|
|
62
|
+
body: parsed.body,
|
|
63
|
+
},
|
|
64
|
+
]);
|
|
65
|
+
if (outcomes[0]?.ok === true) {
|
|
66
|
+
result.imported += 1;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
result.skipped += 1;
|
|
70
|
+
result.errors.push(`${file}: ${outcomes[0]?.reason ?? "unknown rejection"}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
result.skipped += 1;
|
|
75
|
+
result.errors.push(`${file}: unreadable (${error instanceof Error ? error.message : String(error)})`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
const VALID_TYPES = ["user", "feedback", "reference"];
|
|
81
|
+
/**
|
|
82
|
+
* Parse one MD-era file: frontmatter `name` / `description` / `type` (type
|
|
83
|
+
* may sit under a `metadata:` block), body = everything after the closing
|
|
84
|
+
* `---`. Anything missing or malformed is synthesized from the filename and
|
|
85
|
+
* body rather than dropped — migration must not lose.
|
|
86
|
+
*/
|
|
87
|
+
function parseMdMemory(text, fileBase) {
|
|
88
|
+
const fields = { name: "", description: "", type: "" };
|
|
89
|
+
let body = text;
|
|
90
|
+
if (text.startsWith("---")) {
|
|
91
|
+
const blockEnd = text.indexOf("\n---", 3);
|
|
92
|
+
if (blockEnd >= 0) {
|
|
93
|
+
for (const line of text.slice(3, blockEnd).split("\n")) {
|
|
94
|
+
const match = /^(name|description|type):\s*(.*)$/.exec(line.trim());
|
|
95
|
+
if (match?.[1] !== undefined && match?.[2] !== undefined) {
|
|
96
|
+
const key = match[1];
|
|
97
|
+
fields[key] = match[2].trim().replace(/^["']|["']$/g, "");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
body = text.slice(blockEnd + 4);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const title = fields.name.length > 0 ? fields.name : fileBase;
|
|
104
|
+
const type = (VALID_TYPES.includes(fields.type) ? fields.type : "reference");
|
|
105
|
+
const description = fields.description.length > 0 ? fields.description : firstLine(body, 200);
|
|
106
|
+
return {
|
|
107
|
+
id: fields.name.length > 0 && slugifyId(fields.name) === fields.name ? fields.name : slugifyId(title),
|
|
108
|
+
type,
|
|
109
|
+
title,
|
|
110
|
+
description: description.length > 0 ? description : fileBase,
|
|
111
|
+
body: body.trim().length > 0 ? body.trim() : fields.name,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/** First non-empty line, capped — the synthesized relevance hook. */
|
|
115
|
+
function firstLine(text, cap) {
|
|
116
|
+
for (const line of text.split("\n")) {
|
|
117
|
+
const trimmed = line.trim().replace(/^#+\s*/, "").replace(/^-\s*/, "");
|
|
118
|
+
if (trimmed.length > 0) {
|
|
119
|
+
return trimmed.length > cap ? `${trimmed.slice(0, cap - 1)}…` : trimmed;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return "";
|
|
123
|
+
}
|
|
124
|
+
/** Rename the imported directory aside; null when the rename failed. */
|
|
125
|
+
function renameAside(memoryDir) {
|
|
126
|
+
const stamp = new Date().toISOString().slice(0, 10);
|
|
127
|
+
const base = join(memoryDir, "..", `memory-imported-${stamp}`);
|
|
128
|
+
let target = base;
|
|
129
|
+
for (let attempt = 2; existsSync(target); attempt += 1) {
|
|
130
|
+
target = `${base}-${attempt}`;
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
renameSync(memoryDir, target);
|
|
134
|
+
return target;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=import-md.js.map
|
package/lib/index.d.ts
CHANGED
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-continual-evolve — plugin entry.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* background extraction, and no governance. See
|
|
10
|
-
* .agents/notes/implemented/architecture/2026-10-04-zcode-alignment-teardown.md.
|
|
4
|
+
* The v0.15 SQLite single-store redesign (ADR
|
|
5
|
+
* `2026-10-06-sqlite-single-store`): every memory lives in one central
|
|
6
|
+
* database (`~/.dsh/evolve/memory.db`, rows partitioned by workspace path)
|
|
7
|
+
* and the plugin's code is the literal sole writer. This entry wires four
|
|
8
|
+
* surfaces over the store:
|
|
11
9
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
10
|
+
* - the session-start section (`memory-section.ts`): a store query rendered
|
|
11
|
+
* as the injected index, frozen per session for the prompt cache;
|
|
12
|
+
* - the model tools (`memory-tools.ts`): `memory_write` for explicit
|
|
13
|
+
* "记住/忘掉" instructions, `memory_read` for bodies and search;
|
|
14
|
+
* - the extraction scheduler (`extraction.ts`): turn-level, per-session
|
|
15
|
+
* slots, single-flight per session; proposals land only through the
|
|
16
|
+
* store's gates;
|
|
17
|
+
* - the read-only card routes (`card-routes.ts`) in the official plugin
|
|
18
|
+
* manager.
|
|
19
19
|
*
|
|
20
|
-
* The
|
|
21
|
-
*
|
|
22
|
-
* the
|
|
20
|
+
* The store opens asynchronously (dynamic `node:sqlite` import); until it is
|
|
21
|
+
* ready — or forever, if the runtime lacks `node:sqlite` — the section
|
|
22
|
+
* renders "" and the tools never register. Injection must never break an
|
|
23
|
+
* assembly, and a degraded plugin must never take the host down.
|
|
24
|
+
*
|
|
25
|
+
* The host API surface is declared locally (the minimal shapes this plugin
|
|
26
|
+
* touches) instead of importing the DSH type packages — verified in
|
|
27
|
+
* production across every prior release.
|
|
23
28
|
*/
|
|
24
29
|
import z from "@deepseek-ai/schemastery";
|
|
25
30
|
export declare const name = "continual-evolve";
|
|
@@ -43,22 +48,34 @@ interface HostContext {
|
|
|
43
48
|
}
|
|
44
49
|
export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
45
50
|
/**
|
|
46
|
-
* The memory section: injects the workspace memory index
|
|
47
|
-
* start, frozen per session to keep the system prompt
|
|
48
|
-
* (prompt cache). `enabled`/`guide`/`order`/`maxChars`
|
|
49
|
-
* true/true/400/6000; with `guide` off and an empty store the
|
|
50
|
-
* renders to "" and costs no tokens.
|
|
51
|
+
* The memory section: injects the workspace memory index (a store query)
|
|
52
|
+
* at session start, frozen per session to keep the system prompt
|
|
53
|
+
* byte-stable (prompt cache). `enabled`/`guide`/`order`/`maxChars`
|
|
54
|
+
* default to true/true/400/6000; with `guide` off and an empty store the
|
|
55
|
+
* section renders to "" and costs no tokens.
|
|
51
56
|
*/
|
|
52
57
|
memoryIndex: z<Schemastery.ObjectS<NoInfer<{
|
|
53
58
|
enabled: z<boolean, boolean, "plain">;
|
|
54
59
|
guide: z<boolean, boolean, "plain">;
|
|
55
60
|
order: z<number, number, "plain">;
|
|
56
61
|
maxChars: z<number, number, "plain">;
|
|
62
|
+
/**
|
|
63
|
+
* The proposal-based extraction run (trigger: turn-level, per-session
|
|
64
|
+
* slots, no idle timer; writes land only through the store's gates).
|
|
65
|
+
* Off keeps the plugin injection + explicit tools only.
|
|
66
|
+
*/
|
|
67
|
+
extraction: z<boolean, boolean, "plain">;
|
|
57
68
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
58
69
|
enabled: z<boolean, boolean, "plain">;
|
|
59
70
|
guide: z<boolean, boolean, "plain">;
|
|
60
71
|
order: z<number, number, "plain">;
|
|
61
72
|
maxChars: z<number, number, "plain">;
|
|
73
|
+
/**
|
|
74
|
+
* The proposal-based extraction run (trigger: turn-level, per-session
|
|
75
|
+
* slots, no idle timer; writes land only through the store's gates).
|
|
76
|
+
* Off keeps the plugin injection + explicit tools only.
|
|
77
|
+
*/
|
|
78
|
+
extraction: z<boolean, boolean, "plain">;
|
|
62
79
|
}>>, "plain">;
|
|
63
80
|
/**
|
|
64
81
|
* The read-only memory card in the official plugin manager
|
|
@@ -73,22 +90,34 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
73
90
|
}>>, "plain">;
|
|
74
91
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
75
92
|
/**
|
|
76
|
-
* The memory section: injects the workspace memory index
|
|
77
|
-
* start, frozen per session to keep the system prompt
|
|
78
|
-
* (prompt cache). `enabled`/`guide`/`order`/`maxChars`
|
|
79
|
-
* true/true/400/6000; with `guide` off and an empty store the
|
|
80
|
-
* renders to "" and costs no tokens.
|
|
93
|
+
* The memory section: injects the workspace memory index (a store query)
|
|
94
|
+
* at session start, frozen per session to keep the system prompt
|
|
95
|
+
* byte-stable (prompt cache). `enabled`/`guide`/`order`/`maxChars`
|
|
96
|
+
* default to true/true/400/6000; with `guide` off and an empty store the
|
|
97
|
+
* section renders to "" and costs no tokens.
|
|
81
98
|
*/
|
|
82
99
|
memoryIndex: z<Schemastery.ObjectS<NoInfer<{
|
|
83
100
|
enabled: z<boolean, boolean, "plain">;
|
|
84
101
|
guide: z<boolean, boolean, "plain">;
|
|
85
102
|
order: z<number, number, "plain">;
|
|
86
103
|
maxChars: z<number, number, "plain">;
|
|
104
|
+
/**
|
|
105
|
+
* The proposal-based extraction run (trigger: turn-level, per-session
|
|
106
|
+
* slots, no idle timer; writes land only through the store's gates).
|
|
107
|
+
* Off keeps the plugin injection + explicit tools only.
|
|
108
|
+
*/
|
|
109
|
+
extraction: z<boolean, boolean, "plain">;
|
|
87
110
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
88
111
|
enabled: z<boolean, boolean, "plain">;
|
|
89
112
|
guide: z<boolean, boolean, "plain">;
|
|
90
113
|
order: z<number, number, "plain">;
|
|
91
114
|
maxChars: z<number, number, "plain">;
|
|
115
|
+
/**
|
|
116
|
+
* The proposal-based extraction run (trigger: turn-level, per-session
|
|
117
|
+
* slots, no idle timer; writes land only through the store's gates).
|
|
118
|
+
* Off keeps the plugin injection + explicit tools only.
|
|
119
|
+
*/
|
|
120
|
+
extraction: z<boolean, boolean, "plain">;
|
|
92
121
|
}>>, "plain">;
|
|
93
122
|
/**
|
|
94
123
|
* The read-only memory card in the official plugin manager
|