dsh-continual-evolve 0.14.1 → 0.15.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.
@@ -0,0 +1,269 @@
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 and clears the timer. The scheduler is the
169
+ * only background automation the plugin runs, and it is bounded by design:
170
+ * one timer, one in-flight run, one pending slot.
171
+ */
172
+ export function createExtractionScheduler(host, deps, options = {}) {
173
+ const debounceMs = Math.max(0, (options.debounceMin ?? 10) * 60_000);
174
+ const listeners = [];
175
+ let timer = null;
176
+ let running = false;
177
+ let pending = null;
178
+ const targetOf = (agent) => {
179
+ const id = agent?.id;
180
+ if (typeof id !== "string" || id.length === 0) {
181
+ return null;
182
+ }
183
+ const route = agent?.options;
184
+ return {
185
+ agent,
186
+ sessionId: id,
187
+ provider: typeof route?.provider === "string" ? route.provider : undefined,
188
+ model: typeof route?.model === "string" ? route.model : undefined,
189
+ };
190
+ };
191
+ const flush = (trigger) => {
192
+ if (timer !== null) {
193
+ clearTimeout(timer);
194
+ timer = null;
195
+ }
196
+ if (running || pending === null) {
197
+ return;
198
+ }
199
+ const target = pending;
200
+ pending = null;
201
+ running = true;
202
+ void runExtraction(deps, {
203
+ workspaceId: cwdOf(target.agent),
204
+ sessionId: target.sessionId,
205
+ internalAgent: isInternalAgent(target.agent),
206
+ provider: target.provider,
207
+ model: target.model,
208
+ }, { trigger }).catch((error) => {
209
+ host.logger("continual-evolve").warn(`extraction run failed unexpectedly: ${error instanceof Error ? error.message : String(error)}`);
210
+ }).finally(() => {
211
+ running = false;
212
+ if (pending !== null) {
213
+ // A turn arrived mid-run and refreshed the pending slot: drain it.
214
+ flush("turn-debounce");
215
+ }
216
+ });
217
+ };
218
+ const arm = (agent) => {
219
+ const target = targetOf(agent);
220
+ if (target === null) {
221
+ return;
222
+ }
223
+ pending = target; // Coalescing: the latest boundary wins, nothing queues.
224
+ if (debounceMs === 0) {
225
+ flush("turn-debounce");
226
+ return;
227
+ }
228
+ if (timer !== null) {
229
+ clearTimeout(timer);
230
+ }
231
+ timer = setTimeout(() => {
232
+ timer = null;
233
+ flush("turn-debounce");
234
+ }, debounceMs);
235
+ };
236
+ listeners.push(host.on("agent/turn-stopping", (payload) => {
237
+ if (payload.agent === undefined) {
238
+ return;
239
+ }
240
+ arm(payload.agent);
241
+ }));
242
+ listeners.push(host.on("session/event", (session, event) => {
243
+ if (event?.type !== "compaction/start" || pending === null || pending.sessionId !== session?.id) {
244
+ return;
245
+ }
246
+ flush("compaction");
247
+ }));
248
+ listeners.push(host.on("agent/disposed", (payload) => {
249
+ const id = payload.agent?.id;
250
+ if (pending === null || pending.sessionId !== id) {
251
+ return;
252
+ }
253
+ flush("close-drain");
254
+ }));
255
+ host.logger("continual-evolve").info(`extraction scheduler armed (debounce ${options.debounceMin ?? 10}min, single-flight with coalescing)`);
256
+ return () => {
257
+ if (timer !== null) {
258
+ clearTimeout(timer);
259
+ timer = null;
260
+ }
261
+ pending = null;
262
+ for (const listener of listeners) {
263
+ if (typeof listener === "function") {
264
+ listener();
265
+ }
266
+ }
267
+ };
268
+ }
269
+ //# 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
@@ -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,29 @@
1
1
  /**
2
2
  * dsh-continual-evolve — plugin entry.
3
3
  *
4
- * After the 2026-10-04 ZCode-alignment teardown the plugin is one thing: a
5
- * session-start system-prompt section that injects the workspace memory
6
- * index (`<cwd>/.evolve/memory/MEMORY.md`), hands out the store path, and
7
- * teaches the when-to-save guide. The model reads and writes the markdown
8
- * files with its native tools; there are no plugin tools, no commands, no
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
- * The 2026-10-04 plugin-management card adds a read-only surface: the host's
13
- * workspace registry feeds a catalogue, and a `webServer` route triple projects
14
- * any catalogued workspace's store for the card in the official plugin manager
15
- * (`plugins.bundle.config`, client bundle in client/client.js). The card is a
16
- * viewer — see
17
- * .agents/notes/implemented/feature/2026-10-04-plugin-management-card.md and
18
- * .agents/notes/implemented/feature/2026-10-05-zcode-workspace-switcher.md.
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, debounced,
15
+ * single-flight; proposals land only through the store's gates;
16
+ * - the read-only card routes (`card-routes.ts`) in the official plugin
17
+ * manager.
19
18
  *
20
- * The host API surface is declared locally (the minimal shape this plugin
21
- * touches) instead of importing the DSH type packages — the section call is
22
- * the same one every prior release used, verified in production.
19
+ * The store opens asynchronously (dynamic `node:sqlite` import); until it is
20
+ * ready — or forever, if the runtime lacks `node:sqlite` — the section
21
+ * renders "" and the tools never register. Injection must never break an
22
+ * assembly, and a degraded plugin must never take the host down.
23
+ *
24
+ * The host API surface is declared locally (the minimal shapes this plugin
25
+ * touches) instead of importing the DSH type packages — verified in
26
+ * production across every prior release.
23
27
  */
24
28
  import z from "@deepseek-ai/schemastery";
25
29
  export declare const name = "continual-evolve";
@@ -43,22 +47,38 @@ interface HostContext {
43
47
  }
44
48
  export declare const Config: z<Schemastery.ObjectS<NoInfer<{
45
49
  /**
46
- * The memory section: injects the workspace memory index at session
47
- * start, frozen per session to keep the system prompt byte-stable
48
- * (prompt cache). `enabled`/`guide`/`order`/`maxChars` default to
49
- * true/true/400/6000; with `guide` off and an empty store the section
50
- * renders to "" and costs no tokens.
50
+ * The memory section: injects the workspace memory index (a store query)
51
+ * at session start, frozen per session to keep the system prompt
52
+ * byte-stable (prompt cache). `enabled`/`guide`/`order`/`maxChars`
53
+ * default to true/true/400/6000; with `guide` off and an empty store the
54
+ * section renders to "" and costs no tokens.
51
55
  */
52
56
  memoryIndex: z<Schemastery.ObjectS<NoInfer<{
53
57
  enabled: z<boolean, boolean, "plain">;
54
58
  guide: z<boolean, boolean, "plain">;
55
59
  order: z<number, number, "plain">;
56
60
  maxChars: z<number, number, "plain">;
61
+ /**
62
+ * The proposal-based extraction run (trigger: turn-level with idle
63
+ * debounce; writes land only through the store's gates). Off keeps
64
+ * the plugin injection + explicit tools only.
65
+ */
66
+ extraction: z<boolean, boolean, "plain">;
67
+ /** Idle debounce minutes between turn end and an extraction run (0 = per turn). */
68
+ debounceMin: z<number, number, "plain">;
57
69
  }>>, Schemastery.ObjectT<NoInfer<{
58
70
  enabled: z<boolean, boolean, "plain">;
59
71
  guide: z<boolean, boolean, "plain">;
60
72
  order: z<number, number, "plain">;
61
73
  maxChars: z<number, number, "plain">;
74
+ /**
75
+ * The proposal-based extraction run (trigger: turn-level with idle
76
+ * debounce; writes land only through the store's gates). Off keeps
77
+ * the plugin injection + explicit tools only.
78
+ */
79
+ extraction: z<boolean, boolean, "plain">;
80
+ /** Idle debounce minutes between turn end and an extraction run (0 = per turn). */
81
+ debounceMin: z<number, number, "plain">;
62
82
  }>>, "plain">;
63
83
  /**
64
84
  * The read-only memory card in the official plugin manager
@@ -73,22 +93,38 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
73
93
  }>>, "plain">;
74
94
  }>>, Schemastery.ObjectT<NoInfer<{
75
95
  /**
76
- * The memory section: injects the workspace memory index at session
77
- * start, frozen per session to keep the system prompt byte-stable
78
- * (prompt cache). `enabled`/`guide`/`order`/`maxChars` default to
79
- * true/true/400/6000; with `guide` off and an empty store the section
80
- * renders to "" and costs no tokens.
96
+ * The memory section: injects the workspace memory index (a store query)
97
+ * at session start, frozen per session to keep the system prompt
98
+ * byte-stable (prompt cache). `enabled`/`guide`/`order`/`maxChars`
99
+ * default to true/true/400/6000; with `guide` off and an empty store the
100
+ * section renders to "" and costs no tokens.
81
101
  */
82
102
  memoryIndex: z<Schemastery.ObjectS<NoInfer<{
83
103
  enabled: z<boolean, boolean, "plain">;
84
104
  guide: z<boolean, boolean, "plain">;
85
105
  order: z<number, number, "plain">;
86
106
  maxChars: z<number, number, "plain">;
107
+ /**
108
+ * The proposal-based extraction run (trigger: turn-level with idle
109
+ * debounce; writes land only through the store's gates). Off keeps
110
+ * the plugin injection + explicit tools only.
111
+ */
112
+ extraction: z<boolean, boolean, "plain">;
113
+ /** Idle debounce minutes between turn end and an extraction run (0 = per turn). */
114
+ debounceMin: z<number, number, "plain">;
87
115
  }>>, Schemastery.ObjectT<NoInfer<{
88
116
  enabled: z<boolean, boolean, "plain">;
89
117
  guide: z<boolean, boolean, "plain">;
90
118
  order: z<number, number, "plain">;
91
119
  maxChars: z<number, number, "plain">;
120
+ /**
121
+ * The proposal-based extraction run (trigger: turn-level with idle
122
+ * debounce; writes land only through the store's gates). Off keeps
123
+ * the plugin injection + explicit tools only.
124
+ */
125
+ extraction: z<boolean, boolean, "plain">;
126
+ /** Idle debounce minutes between turn end and an extraction run (0 = per turn). */
127
+ debounceMin: z<number, number, "plain">;
92
128
  }>>, "plain">;
93
129
  /**
94
130
  * The read-only memory card in the official plugin manager