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/lib/index.js CHANGED
@@ -1,46 +1,59 @@
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, 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 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.
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
  import { mountCardRoutes } from "./card-routes.js";
26
- import { createKnownWorkspaces } from "./known-workspaces.js";
31
+ import { createExtractionScheduler } from "./extraction.js";
27
32
  import { createFrozenMemorySection, cwdOf, DEFAULT_MAX_CHARS, DEFAULT_MEMORY_SECTION_ORDER, MEMORY_SECTION_NAME, memorySectionText, } from "./memory-section.js";
28
- import { createWorkspaceCatalog } from "./workspace-catalog.js";
33
+ import { registerMemoryTools } from "./memory-tools.js";
34
+ import { importWorkspaceMd } from "./import-md.js";
35
+ import { openMemoryStore } from "./store.js";
29
36
  export const name = "continual-evolve";
30
37
  export const inject = ["systemPrompt"];
31
38
  export const Config = z.object({
32
39
  /**
33
- * The memory section: injects the workspace memory index at session
34
- * start, frozen per session to keep the system prompt byte-stable
35
- * (prompt cache). `enabled`/`guide`/`order`/`maxChars` default to
36
- * true/true/400/6000; with `guide` off and an empty store the section
37
- * renders to "" and costs no tokens.
40
+ * The memory section: injects the workspace memory index (a store query)
41
+ * at session start, frozen per session to keep the system prompt
42
+ * byte-stable (prompt cache). `enabled`/`guide`/`order`/`maxChars`
43
+ * default to true/true/400/6000; with `guide` off and an empty store the
44
+ * section renders to "" and costs no tokens.
38
45
  */
39
46
  memoryIndex: z.object({
40
47
  enabled: z.boolean(),
41
48
  guide: z.boolean(),
42
49
  order: z.natural(),
43
50
  maxChars: z.natural(),
51
+ /**
52
+ * The proposal-based extraction run (trigger: turn-level, per-session
53
+ * slots, no idle timer; writes land only through the store's gates).
54
+ * Off keeps the plugin injection + explicit tools only.
55
+ */
56
+ extraction: z.boolean(),
44
57
  }),
45
58
  /**
46
59
  * The read-only memory card in the official plugin manager
@@ -52,15 +65,60 @@ export const Config = z.object({
52
65
  enabled: z.boolean(),
53
66
  }),
54
67
  });
68
+ /** Register the memory tools through a nested inject (see memory-tools.ts). */
69
+ function mountToolsWhenAvailable(host, store, state) {
70
+ const nested = host;
71
+ if (typeof nested.inject !== "function") {
72
+ host.logger("continual-evolve").warn("host context exposes no inject — memory tools not registered (injection unaffected; explicit memory management falls back to telling the user)");
73
+ return;
74
+ }
75
+ nested.inject(["tools"], (scoped) => {
76
+ const tools = scoped.tools;
77
+ if (tools === undefined || typeof tools.register !== "function") {
78
+ host.logger("continual-evolve").warn("tools service present but empty — memory tools not registered (injection unaffected)");
79
+ return;
80
+ }
81
+ registerMemoryTools(tools, store);
82
+ state.toolsAvailable = true;
83
+ host.logger("continual-evolve").info("memory_write and memory_read registered");
84
+ });
85
+ }
86
+ /**
87
+ * Arm the extraction scheduler through a nested inject: the session reader
88
+ * and LLM services are siblings, so they are resolved inside the scoped
89
+ * context while the event listeners use the plugin's own context. A host
90
+ * without either service (or without `.on`) simply never arms — injection
91
+ * and the explicit tools remain functional.
92
+ */
93
+ function mountExtractionWhenAvailable(host, store, config) {
94
+ const log = host.logger("continual-evolve");
95
+ if (config.memoryIndex?.extraction === false) {
96
+ log.info("extraction disabled by config — injection and memory_write remain active");
97
+ return;
98
+ }
99
+ const nested = host;
100
+ const events = host;
101
+ if (typeof nested.inject !== "function" || typeof events.on !== "function") {
102
+ log.warn("host context exposes no inject/on — extraction not armed (injection and tools unaffected)");
103
+ return;
104
+ }
105
+ nested.inject(["sessionQuery", "llm"], (scoped) => {
106
+ const context = scoped;
107
+ const surface = context.sessionQuery;
108
+ const llm = (context.llm ?? context.get?.("llm"));
109
+ if (surface === undefined || typeof surface.readSurface !== "function") {
110
+ log.warn("sessionQuery service present but empty — extraction not armed (injection and tools unaffected)");
111
+ return;
112
+ }
113
+ createExtractionScheduler(events, { store, surface, llm });
114
+ });
115
+ }
55
116
  /**
56
117
  * Mount the card routes on the host web server through a nested inject, so
57
- * the plugin stays mountable on hosts without that service. Called only when
58
- * the card is enabled. The workspace registry is looked up inside the scoped
59
- * context (nothing else injects it: a host without the service must still get
60
- * the card) and read per request, so a workspace created while the card is
61
- * open appears on the next refresh.
118
+ * the plugin stays mountable on hosts without that service. Called once the
119
+ * store is ready — the routes are projections of the store, not of files.
62
120
  */
63
- function mountCardWhenAvailable(host, served) {
121
+ function mountCardWhenAvailable(host, store) {
64
122
  const nested = host;
65
123
  if (typeof nested.inject !== "function") {
66
124
  host.logger("continual-evolve").warn("host context exposes no inject — memory card routes not mounted (memory section unaffected)");
@@ -72,60 +130,57 @@ function mountCardWhenAvailable(host, served) {
72
130
  host.logger("continual-evolve").warn("webServer service present but empty — memory card routes not mounted (memory section unaffected)");
73
131
  return;
74
132
  }
75
- const catalog = createWorkspaceCatalog({
76
- registry: registryLookup(scoped),
77
- served: served,
78
- homeDir: process.env.DSH_HOME,
79
- });
80
- mountCardRoutes(webServer, catalog);
133
+ mountCardRoutes(webServer, store);
81
134
  });
82
135
  }
83
- /**
84
- * A live `ctx.workspaceRegistry` getter over the scoped context. The service
85
- * is a sibling, so it is reached with `ctx.get` (property access only walks
86
- * fiber ancestors) and treated as optional: an absent, foreign or throwing
87
- * service degrades the catalogue to its fallback sources instead of taking the
88
- * card down.
89
- */
90
- function registryLookup(scoped) {
91
- const context = scoped;
92
- return () => {
93
- if (typeof context.get !== "function") {
94
- return undefined;
95
- }
96
- try {
97
- const service = context.get("workspaceRegistry");
98
- return service === undefined || service === null ? undefined : service;
99
- }
100
- catch {
101
- return undefined;
102
- }
103
- };
104
- }
105
136
  export function apply(host, config) {
106
137
  if (config.memoryIndex?.enabled === false) {
107
138
  return;
108
139
  }
109
- const workspaces = createKnownWorkspaces();
140
+ const log = host.logger("continual-evolve");
141
+ const state = { store: undefined, toolsAvailable: false };
142
+ void openMemoryStore().then((store) => {
143
+ state.store = store;
144
+ mountToolsWhenAvailable(host, store, state);
145
+ mountExtractionWhenAvailable(host, store, config);
146
+ if (config.memoryCard?.enabled !== false) {
147
+ mountCardWhenAvailable(host, store);
148
+ }
149
+ }, (error) => {
150
+ // node:sqlite missing (old runtime) or the database unusable:
151
+ // degrade to a no-op plugin, never take the host down.
152
+ log.warn(`memory store unavailable — injection and tools disabled: ${error instanceof Error ? error.message : String(error)}`);
153
+ });
110
154
  const frozen = createFrozenMemorySection();
111
155
  host.systemPrompt.section({
112
156
  name: MEMORY_SECTION_NAME,
113
157
  order: config.memoryIndex?.order ?? DEFAULT_MEMORY_SECTION_ORDER,
114
158
  text: (context) => {
159
+ const store = state.store;
160
+ if (store === undefined) {
161
+ // Still loading or unavailable: render nothing (and freeze
162
+ // nothing — the first assembly after readiness builds for real).
163
+ return "";
164
+ }
115
165
  const cwd = cwdOf(context.agent);
116
166
  if (cwd !== undefined) {
117
- workspaces.remember(cwd);
167
+ // First contact with this workspace migrates any legacy MD-era
168
+ // store into the database; flag-guarded, so it is a no-op after.
169
+ try {
170
+ importWorkspaceMd(store, cwd);
171
+ }
172
+ catch (error) {
173
+ log.warn(`legacy memory import failed for ${cwd}: ${error instanceof Error ? error.message : String(error)}`);
174
+ }
118
175
  }
119
176
  return frozen.textFor(context.agent, (agent) => memorySectionText(agent, {
177
+ store,
120
178
  maxChars: config.memoryIndex?.maxChars ?? DEFAULT_MAX_CHARS,
121
179
  guide: config.memoryIndex?.guide ?? true,
180
+ toolsAvailable: state.toolsAvailable,
122
181
  }));
123
182
  },
124
183
  });
125
- host.logger("continual-evolve").info("continual-evolve memory section registered (workspace .evolve/memory, native file read/write)");
126
- if (config.memoryCard?.enabled === false) {
127
- return;
128
- }
129
- mountCardWhenAvailable(host, workspaces);
184
+ log.info("continual-evolve memory section registered (central SQLite store, sole-writer gates)");
130
185
  }
131
186
  //# sourceMappingURL=index.js.map
@@ -1,18 +1,26 @@
1
1
  /**
2
- * The persistent-memory guide injected as part of the memory section.
2
+ * The persistent-memory guide injected as part of the memory section —
3
+ * three sentences of discipline, nothing more (the v0.15 SQLite single-store
4
+ * redesign, ADR `2026-10-06-sqlite-single-store`).
3
5
  *
4
- * This text is the write-path half of the ZCode-aligned design: it teaches
5
- * the model when a durable memory is worth creating, which type it belongs
6
- * to, what body structure each type owes, and that memories are plain
7
- * markdown files written directly with the native file tools — the plugin
8
- * registers no tools and no commands.
9
- *
10
- * Provenance: the when_to_save taxonomy is adapted from ZCode's persistent-
11
- * memory prompt (introduced here in evolve v2); the write-path wording now
12
- * targets native file operations on `<workspace>/.evolve/memory/`.
6
+ * The store is a central database only the plugin's code can write; the
7
+ * model reads the injected index, fetches bodies with `memory_read`, and
8
+ * lands explicit writes through `memory_write`. Automatic sedimentation is
9
+ * the extraction pipeline's job and the main session must not spend tokens
10
+ * maintaining memory. The write-quality taxonomy (what deserves a memory,
11
+ * type bodies, the ADR boundary) moved to the extraction prompt, which is
12
+ * the only writer left that needs it.
13
+ */
14
+ /**
15
+ * Heading and framing before the `<memories>` block. `toolsAvailable`
16
+ * reflects whether the tool registration actually succeeded on this host:
17
+ * a host without the tools service degrades to "tell the user" phrasing
18
+ * instead of naming tools that do not exist.
19
+ */
20
+ export declare function memoryGuideIntro(storePath: string, workspaceId: string, toolsAvailable: boolean): string;
21
+ /**
22
+ * Hard rules that ride along regardless of the intro (kept as a separate
23
+ * constant so the section renderer can compose them once).
13
24
  */
14
- /** Heading and framing before the `<memories>` block. Takes the absolute memory dir. */
15
- export declare function memoryGuideIntro(memoryDir: string): string;
16
- /** The file format, when_to_save / how_to_use rules, and maintenance discipline. */
17
- export declare const MEMORY_GUIDE_RULES = "## \u8BB0\u5FC6\u6587\u4EF6\u7684\u683C\u5F0F\n\n---\nname: <\u77ED\u6A2A\u7EBF\u5C0F\u5199\u6807\u8BC6>\ndescription: <\u4E00\u53E5\u8BDD\u76F8\u5173\u6027\u94A9\u5B50\u2014\u2014\u51B3\u5B9A\u672A\u6765\u4F1A\u8BDD\u4F1A\u4E0D\u4F1A\u60F3\u8D77\u8FD9\u6761\u8BB0\u5FC6>\nmetadata:\n type: user | feedback | project | reference\n---\n\n<\u6B63\u6587>\n\n## \u4F55\u65F6\u5199\u5165\u8BB0\u5FC6\n\n\u7528\u6237\u660E\u786E\u8981\u6C42\u8BB0\u4F4F\u65F6\uFF0C\u7ACB\u5373\u5B58\u4E3A\u6700\u5408\u9002\u7684\u7C7B\u578B\uFF1B\u8981\u6C42\u5FD8\u8BB0\u65F6\uFF0C\u627E\u5230\u5E76\u5220\u9664\u76F8\u5173\u6587\u4EF6\u4E0E\u7D22\u5F15\u884C\u3002\n\u9664\u6B64\u4E4B\u5916\uFF0C\u5728\u5BF9\u8BDD\u4E2D\u81EA\u7136\u9047\u5230\u4EE5\u4E0B\u4FE1\u53F7\u65F6\u4E3B\u52A8\u5199\u5165\u3002\u5B58\u4E4B\u524D\u5148\u8FC7\u4E00\u9053\u7B5B\u5B50\uFF1A\n**\u80FD\u4ECE\u4EE3\u7801\u3001git \u5386\u53F2\u3001\u4ED3\u5E93\u6587\u4EF6\u91CC\u91CD\u65B0\u63A8\u5BFC\u51FA\u6765\u7684\u4E8B\u5B9E\u4E0D\u5B58**\u2014\u2014\u53EA\u5B58\u91CD\u63A8\u5BFC\u4E0D\u51FA\u6765\u7684\n\u80CC\u666F\u3001\u51B3\u7B56\u548C\u504F\u597D\u3002\u76F8\u5BF9\u65E5\u671F\u4E00\u5F8B\u8F6C\u6210\u7EDD\u5BF9\u65E5\u671F\uFF08\"\u5468\u56DB\"\u2192\"2026-10-08\"\uFF09\uFF0C\u5426\u5219\u672A\u6765\n\u7684\u4F1A\u8BDD\u65E0\u6CD5\u89E3\u8BFB\u3002\u4E00\u6761\u4E8B\u5B9E\u4E00\u4E2A\u6587\u4EF6\uFF1B\u4E0D\u786E\u5B9A\u503C\u4E0D\u503C\u5F97\u5B58\u65F6\uFF0C\u95EE\u81EA\u5DF1\"\u8FD9\u6761\u7684\u4FE1\u606F\u91CF\n\u662F\u5426\u5728'\u4E3A\u4EC0\u4E48'\u91CC\uFF0C\u800C\u4E0D\u5728'\u662F\u4EC0\u4E48'\u91CC\"\u3002\n\n<type name=\"user\">\n <when_to_save>\u5F97\u77E5\u7528\u6237\u7684\u89D2\u8272\u3001\u6280\u672F\u6808\u3001\u7ECF\u9A8C\u6C34\u5E73\u3001\u504F\u597D\u6216\u77E5\u8BC6\u80CC\u666F\u7684\u4EFB\u4F55\u7EC6\u8282\u65F6\u3002\n \u4F8B\u5982\u7528\u6237\u81EA\u8FF0\"\u6211\u662F\u6570\u636E\u79D1\u5B66\u5BB6\uFF0C\u5728\u67E5\u65E5\u5FD7\u7CFB\u7EDF\"\u2014\u2014\u5B58\uFF1A\u7528\u6237\u662F\u6570\u636E\u79D1\u5B66\u5BB6\uFF0C\n \u5F53\u524D\u5173\u6CE8\u53EF\u89C2\u6D4B\u6027/\u65E5\u5FD7\u3002</when_to_save>\n <how_to_use>\u540E\u7EED\u89E3\u91CA\u548C\u5EFA\u8BAE\u6309\u7528\u6237\u7684\u80CC\u666F\u5B9A\u5236\u6DF1\u6D45\u4E0E\u7C7B\u6BD4\uFF1B\u5BF9\u8D44\u6DF1\u5DE5\u7A0B\u5E08\n \u4E0D\u89E3\u91CA\u57FA\u7840\u6982\u5FF5\uFF0C\u5BF9\u67D0\u9886\u57DF\u7684\u65B0\u624B\u7528\u5176\u719F\u6089\u9886\u57DF\u7684\u7C7B\u6BD4\u5207\u5165\u3002</how_to_use>\n</type>\n\n<type name=\"feedback\">\n <when_to_save>\u4E24\u6761\u4FE1\u53F7\uFF0C\u7F3A\u4E00\u4E0D\u53EF\uFF1A\n \uFF081\uFF09\u7EA0\u6B63\u2014\u2014\u7528\u6237\u8BF4\"\u4E0D\u662F\u8FD9\u6837\"\"\u522B\"\"\u505C\u4E0B\"\u65F6\uFF0C\u5B58\u88AB\u7EA0\u6B63\u7684\u505A\u6CD5\u4E0E\u6B63\u786E\u505A\u6CD5\uFF1B\n \uFF082\uFF09\u786E\u8BA4\u2014\u2014\u8FD9\u662F\u66F4\u5B89\u9759\u3001\u66F4\u5BB9\u6613\u6F0F\u7684\u4FE1\u53F7\uFF1A\u7528\u6237\u63A5\u53D7\u4E86\u4F60\u4E0D\u5BFB\u5E38\u7684\u9009\u62E9\u800C\u6CA1\u6709\u5F02\u8BAE\uFF0C\n \u6216\u660E\u786E\u8BF4\"\u5BF9\uFF0C\u5C31\u8FD9\u6837\"\"\u4FDD\u6301\u8FD9\u6837\"\u3002\u786E\u8BA4\u8FC7\u7684\u5224\u65AD\u662F\u5DF2\u9A8C\u8BC1\u7684\u65B9\u6CD5\u8BBA\uFF0C\u53EA\u5B58\u7EA0\u9519\n \u4F1A\u8BA9\u4F60\u7684\u884C\u4E3A\u8D8A\u6765\u8D8A\u4FDD\u5B88\u3001\u504F\u79BB\u7528\u6237\u5DF2\u8BA4\u53EF\u7684\u8DEF\u3002\n \u4F8B\uFF1A\"\u6D4B\u8BD5\u522B mock \u6570\u636E\u5E93\uFF0C\u4E0A\u6B21 mock \u8FC7\u4E86\u7EBF\u4E0A\u8FC1\u79FB\u6302\u4E86\"\u2014\u2014\u5B58\u89C4\u5219\u5E76\u5E26\u4E0A\u4E8B\u6545\u539F\u56E0\u3002\n \u4F8B\uFF1A\"\u7ED3\u5C3E\u4E0D\u7528\u603B\u7ED3\uFF0Cdiff \u6211\u81EA\u5DF1\u4F1A\u770B\"\u2014\u2014\u5B58\uFF1A\u8BE5\u7528\u6237\u4E0D\u8981\u5C3E\u968F\u6027\u603B\u7ED3\u3002</when_to_save>\n <how_to_use>\u8BA9\u7528\u6237\u4E0D\u5FC5\u628A\u540C\u4E00\u6761\u6307\u5BFC\u8BF4\u7B2C\u4E8C\u904D\u3002</how_to_use>\n <body_structure>\u89C4\u5219\u5148\u884C\uFF0C\u968F\u540E\u5FC5\u5E26\u4E24\u884C\uFF1A**Why:**\uFF08\u7528\u6237\u7ED9\u7684\u7406\u7531\uFF0C\u5E38\u662F\u67D0\u6B21\n \u4E8B\u6545\u6216\u5F3A\u504F\u597D\u2014\u2014\u77E5\u9053\u4E3A\u4EC0\u4E48\u624D\u80FD\u5224\u65AD\u8FB9\u7F18\u60C5\u51B5\u8BE5\u4E0D\u8BE5\u7834\u4F8B\uFF09\u548C **How to apply:**\n \uFF08\u4F55\u65F6\u4F55\u5730\u751F\u6548\uFF09\u3002feedback \u7C7B\u578B\u7F3A\u8FD9\u4E24\u884C\u89C6\u4E3A\u4E0D\u5B8C\u6574\u3002</body_structure>\n</type>\n\n<type name=\"project\">\n <when_to_save>\u5F97\u77E5\u8FDB\u884C\u4E2D\u7684\u5DE5\u4F5C\u3001\u76EE\u6807\u3001\u51B3\u7B56\u3001\u622A\u6B62\u671F\u6216\u4E8B\u6545\u7684\u80CC\u666F\u52A8\u673A\u65F6\u2014\u2014\n \u51E1\u662F\"\u8C01\u5728\u505A\u4EC0\u4E48\u3001\u4E3A\u4EC0\u4E48\u3001\u5230\u4EC0\u4E48\u65F6\u5019\"\u4E14\u65E0\u6CD5\u4ECE\u4EE3\u7801\u6216 git \u63A8\u5BFC\u7684\u4FE1\u606F\u3002\n \u8FD9\u7C7B\u8BB0\u5FC6\u8870\u51CF\u5FEB\uFF0C\u72B6\u6001\u53D8\u5316\u65F6\u66F4\u65B0\u5B83\uFF0C\u522B\u5806\u53E0\u65B0\u6587\u4EF6\u3002</when_to_save>\n <how_to_use>\u7528\u5B83\u8865\u5168\u8BF7\u6C42\u80CC\u540E\u7684\u4E0A\u4E0B\u6587\u4E0E\u52A8\u673A\uFF0C\u8BA9\u5EFA\u8BAE\u8D34\u4F4F\u771F\u5B9E\u7EA6\u675F\u3002\n \u4F8B\uFF1A\"\u5468\u56DB\u8D77\u51BB\u7ED3\u5408\u5E76\"\u2014\u2014\u5B58\uFF1A2026-03-05 \u8D77\u51BB\u7ED3\u5408\u5E76\uFF0C\u975E\u5173\u952E PR \u63D0\u524D\u6807\u8BB0\u3002</how_to_use>\n <body_structure>\u4E8B\u5B9E/\u51B3\u7B56\u5148\u884C + **Why:**\uFF08\u52A8\u673A\uFF1A\u7EA6\u675F\u3001deadline\u3001\u5E72\u7CFB\u4EBA\u8981\u6C42\uFF09\n + **How to apply:**\uFF08\u8FD9\u6761\u5982\u4F55\u5F71\u54CD\u540E\u7EED\u5EFA\u8BAE\uFF09\u3002</body_structure>\n</type>\n\n<type name=\"reference\">\n <when_to_save>\u7528\u6237\u63D0\u5230\u672A\u6765\u4F1A\u7528\u5230\u7684\u5916\u90E8\u8D44\u6E90\u6307\u9488\uFF1AURL\u3001\u4EEA\u8868\u76D8\u3001\u5DE5\u5355\u3001\n \u6587\u6863\u4F4D\u7F6E\u3002</when_to_save>\n <how_to_use>\u9700\u8981\u65F6\u76F4\u63A5\u53D6\u7528\uFF0C\u4E0D\u91CD\u65B0\u68C0\u7D22\u3002</how_to_use>\n</type>\n\n## \u5982\u4F55\u7EF4\u62A4\n\n- \u5199\u5B8C\u8BB0\u5FC6\u6587\u4EF6\u540E\u7ACB\u5373\u5728 MEMORY.md \u8FFD\u52A0\u6216\u66F4\u65B0\u4E00\u884C\u7D22\u5F15\uFF1A`- [\u6807\u9898](\u6587\u4EF6\u540D.md) \u2014 \u94A9\u5B50`\uFF1B\u72B6\u6001\u53D8\u5316\u65F6\u539F\u6587\u4E0E\u7D22\u5F15\u884C\u4E00\u8D77\u6539\uFF0C\u522B\u8BA9\u7D22\u5F15\u8BF4\u8C0E\u3002\n- \u66F4\u65B0\u4F18\u5148\u4E8E\u65B0\u5EFA\uFF1A\u72B6\u6001\u53D8\u4E86\u6539\u539F\u6587\u4EF6\uFF0C\u4E0D\u53E6\u8D77\u4E00\u6761\uFF08\u65B0\u65E7\u5E76\u5B58\u6BD4\u6CA1\u6709\u8BB0\u5FC6\u66F4\u7CDF\uFF09\u3002\n- \u5F15\u7528\u8BB0\u5FC6\u524D\u82E5\u53D1\u73B0\u4E0E\u73B0\u5B9E\u4E0D\u7B26\uFF0C\u5F53\u573A\u4FEE\u6B63\u6216\u5220\u9664\u8BE5\u6587\u4EF6\u4E0E\u7D22\u5F15\u884C\u2014\u2014\u574F\u8BB0\u5FC6\u7684\u5371\u5BB3\u968F\u5F15\u7528\u6269\u6563\uFF0C\n \u9000\u5F79\u4E0D\u7559\u7ED9\u7528\u6237\u3002\n- [[\u540D\u5B57]] \u4E92\u76F8\u5F15\u7528\u76F8\u5173\u8BB0\u5FC6\uFF1B\u5F15\u7528\u8FD8\u4E0D\u5B58\u5728\u7684\u540D\u5B57\u662F\u5408\u6CD5\u7684\uFF0C\u5B83\u6807\u8BB0\"\u503C\u5F97\u5C06\u6765\u8865\u5199\"\u3002";
25
+ export declare const MEMORY_GUIDE_RULES = "**\u8FB9\u754C\uFF1A\u6362\u4E00\u4E2A\u4ED3\u5E93\u8FD8\u6709\u7528\u7684\u4FE1\u606F\u624D\u8FDB\u8BB0\u5FC6\u3002**\u51B3\u7B56\u3001\u67B6\u6784\u53D6\u820D\u3001\u8FDB\u884C\u4E2D\u72B6\u6001\u8D70\u4ED3\u5E93\u7684 ADR/\u6587\u6863\u8DEF\u7EBF\uFF0C\u4E0D\u8FDB\u8BB0\u5FC6\u5E93\uFF1B\u76F8\u5BF9\u65E5\u671F\u4E00\u5F8B\u5199\u6210\u7EDD\u5BF9\u65E5\u671F\uFF08\"\u5468\u56DB\"\u2192\"2026-10-08\"\uFF09\u3002";
18
26
  //# sourceMappingURL=memory-guide.d.ts.map
@@ -1,86 +1,43 @@
1
1
  /**
2
- * The persistent-memory guide injected as part of the memory section.
2
+ * The persistent-memory guide injected as part of the memory section —
3
+ * three sentences of discipline, nothing more (the v0.15 SQLite single-store
4
+ * redesign, ADR `2026-10-06-sqlite-single-store`).
3
5
  *
4
- * This text is the write-path half of the ZCode-aligned design: it teaches
5
- * the model when a durable memory is worth creating, which type it belongs
6
- * to, what body structure each type owes, and that memories are plain
7
- * markdown files written directly with the native file tools — the plugin
8
- * registers no tools and no commands.
9
- *
10
- * Provenance: the when_to_save taxonomy is adapted from ZCode's persistent-
11
- * memory prompt (introduced here in evolve v2); the write-path wording now
12
- * targets native file operations on `<workspace>/.evolve/memory/`.
6
+ * The store is a central database only the plugin's code can write; the
7
+ * model reads the injected index, fetches bodies with `memory_read`, and
8
+ * lands explicit writes through `memory_write`. Automatic sedimentation is
9
+ * the extraction pipeline's job and the main session must not spend tokens
10
+ * maintaining memory. The write-quality taxonomy (what deserves a memory,
11
+ * type bodies, the ADR boundary) moved to the extraction prompt, which is
12
+ * the only writer left that needs it.
13
13
  */
14
- /** Heading and framing before the `<memories>` block. Takes the absolute memory dir. */
15
- export function memoryGuideIntro(memoryDir) {
16
- return `# 持久记忆
17
-
18
- 你有一个跨会话的持久记忆系统,存放在 \`${memoryDir}/\`:一事一文,\`MEMORY.md\` 是索引,其内容已注入在下方。没有专用命令或工具——直接用你的文件读写工具查看、新建、修改、删除记忆文件,并在同一会话内顺手落盘。`;
19
- }
20
- /** The file format, when_to_save / how_to_use rules, and maintenance discipline. */
21
- export const MEMORY_GUIDE_RULES = `## 记忆文件的格式
22
-
23
- ---
24
- name: <短横线小写标识>
25
- description: <一句话相关性钩子——决定未来会话会不会想起这条记忆>
26
- metadata:
27
- type: user | feedback | project | reference
28
- ---
29
-
30
- <正文>
31
-
32
- ## 何时写入记忆
33
-
34
- 用户明确要求记住时,立即存为最合适的类型;要求忘记时,找到并删除相关文件与索引行。
35
- 除此之外,在对话中自然遇到以下信号时主动写入。存之前先过一道筛子:
36
- **能从代码、git 历史、仓库文件里重新推导出来的事实不存**——只存重推导不出来的
37
- 背景、决策和偏好。相对日期一律转成绝对日期("周四"→"2026-10-08"),否则未来
38
- 的会话无法解读。一条事实一个文件;不确定值不值得存时,问自己"这条的信息量
39
- 是否在'为什么'里,而不在'是什么'里"。
14
+ /**
15
+ * Heading and framing before the `<memories>` block. `toolsAvailable`
16
+ * reflects whether the tool registration actually succeeded on this host:
17
+ * a host without the tools service degrades to "tell the user" phrasing
18
+ * instead of naming tools that do not exist.
19
+ */
20
+ export function memoryGuideIntro(storePath, workspaceId, toolsAvailable) {
21
+ if (toolsAvailable) {
22
+ return `# 持久记忆
40
23
 
41
- <type name="user">
42
- <when_to_save>得知用户的角色、技术栈、经验水平、偏好或知识背景的任何细节时。
43
- 例如用户自述"我是数据科学家,在查日志系统"——存:用户是数据科学家,
44
- 当前关注可观测性/日志。</when_to_save>
45
- <how_to_use>后续解释和建议按用户的背景定制深浅与类比;对资深工程师
46
- 不解释基础概念,对某领域的新手用其熟悉领域的类比切入。</how_to_use>
47
- </type>
24
+ 你的持久记忆存放在中央库 \`${storePath}\`(按工作区分区,当前工作区:\`${workspaceId}\`)。下方注入的是本工作区的记忆索引(feedback > user > reference,只有钩子不含正文);需要某条正文或想按关键词检索,用 \`memory_read\`。
48
25
 
49
- <type name="feedback">
50
- <when_to_save>两条信号,缺一不可:
51
- (1)纠正——用户说"不是这样""别""停下"时,存被纠正的做法与正确做法;
52
- (2)确认——这是更安静、更容易漏的信号:用户接受了你不寻常的选择而没有异议,
53
- 或明确说"对,就这样""保持这样"。确认过的判断是已验证的方法论,只存纠错
54
- 会让你的行为越来越保守、偏离用户已认可的路。
55
- 例:"测试别 mock 数据库,上次 mock 过了线上迁移挂了"——存规则并带上事故原因。
56
- 例:"结尾不用总结,diff 我自己会看"——存:该用户不要尾随性总结。</when_to_save>
57
- <how_to_use>让用户不必把同一条指导说第二遍。</how_to_use>
58
- <body_structure>规则先行,随后必带两行:**Why:**(用户给的理由,常是某次
59
- 事故或强偏好——知道为什么才能判断边缘情况该不该破例)和 **How to apply:**
60
- (何时何地生效)。feedback 类型缺这两行视为不完整。</body_structure>
61
- </type>
26
+ 用户明确要求记住时,用 \`memory_write\`(action: create)落库——feedback 类的正文必带 **Why:** 与 **How to apply:** 两行;要求忘记时用 action: delete;情况变了用 action: update 改原条目,不另起新条目。
62
27
 
63
- <type name="project">
64
- <when_to_save>得知进行中的工作、目标、决策、截止期或事故的背景动机时——
65
- 凡是"谁在做什么、为什么、到什么时候"且无法从代码或 git 推导的信息。
66
- 这类记忆衰减快,状态变化时更新它,别堆叠新文件。</when_to_save>
67
- <how_to_use>用它补全请求背后的上下文与动机,让建议贴住真实约束。
68
- 例:"周四起冻结合并"——存:2026-03-05 起冻结合并,非关键 PR 提前标记。</how_to_use>
69
- <body_structure>事实/决策先行 + **Why:**(动机:约束、deadline、干系人要求)
70
- + **How to apply:**(这条如何影响后续建议)。</body_structure>
71
- </type>
28
+ 除此之外的沉淀由会话结束后的专职提取流程负责——不要在执行任务途中分心维护记忆。`;
29
+ }
30
+ return `# 持久记忆
72
31
 
73
- <type name="reference">
74
- <when_to_save>用户提到未来会用到的外部资源指针:URL、仪表盘、工单、
75
- 文档位置。</when_to_save>
76
- <how_to_use>需要时直接取用,不重新检索。</how_to_use>
77
- </type>
32
+ 你的持久记忆存放在中央库 \`${storePath}\`(按工作区分区,当前工作区:\`${workspaceId}\`),但本宿主未挂载记忆读写工具。下方注入的是本工作区的记忆索引(feedback > user > reference,只有钩子不含正文);需要正文时,把 id 告诉用户请其代查。
78
33
 
79
- ## 如何维护
34
+ 用户明确要求记住或忘记时,直接把要记的内容和类型(user/feedback/reference)告诉用户,由用户手动管理。
80
35
 
81
- - 写完记忆文件后立即在 MEMORY.md 追加或更新一行索引:\`- [标题](文件名.md) — 钩子\`;状态变化时原文与索引行一起改,别让索引说谎。
82
- - 更新优先于新建:状态变了改原文件,不另起一条(新旧并存比没有记忆更糟)。
83
- - 引用记忆前若发现与现实不符,当场修正或删除该文件与索引行——坏记忆的危害随引用扩散,
84
- 退役不留给用户。
85
- - [[名字]] 互相引用相关记忆;引用还不存在的名字是合法的,它标记"值得将来补写"。`;
36
+ 除此之外的沉淀由会话结束后的专职提取流程负责——不要在执行任务途中分心维护记忆。`;
37
+ }
38
+ /**
39
+ * Hard rules that ride along regardless of the intro (kept as a separate
40
+ * constant so the section renderer can compose them once).
41
+ */
42
+ export const MEMORY_GUIDE_RULES = `**边界:换一个仓库还有用的信息才进记忆。**决策、架构取舍、进行中状态走仓库的 ADR/文档路线,不进记忆库;相对日期一律写成绝对日期("周四"→"2026-10-08")。`;
86
43
  //# sourceMappingURL=memory-guide.js.map
@@ -0,0 +1,56 @@
1
+ /** Memory types a record can carry (the guide's taxonomy, enforced). */
2
+ export declare const MEMORY_TYPES: readonly ["user", "feedback", "reference"];
3
+ export type MemoryType = (typeof MEMORY_TYPES)[number];
4
+ /** Record lifecycle statuses. `quarantined` is set by patrol, never shown. */
5
+ export declare const MEMORY_STATUSES: readonly ["active", "quarantined", "archived"];
6
+ export type MemoryStatus = (typeof MEMORY_STATUSES)[number];
7
+ /** Hard body size cap: the one unbounded-growth entry point, killed at write. */
8
+ export declare const MEMORY_BODY_LIMIT = 65536;
9
+ /** Sanity caps for the injection-facing fields (hook text, not prose). */
10
+ export declare const MEMORY_TITLE_LIMIT = 200;
11
+ export declare const MEMORY_DESCRIPTION_LIMIT = 500;
12
+ /** Full id bound (slug + hash-fallback suffix headroom). */
13
+ export declare const MEMORY_ID_LIMIT = 80;
14
+ /**
15
+ * First reason the content reads as carrying a live credential, or undefined
16
+ * when it screens clean. Applied to the whole proposed record (title +
17
+ * description + body) at write time, and again by patrol over stored rows.
18
+ */
19
+ export declare function secretLeakReason(content: string): string | undefined;
20
+ /**
21
+ * Derive a kebab-slug id from a title. CJK (or any non-ASCII-only) titles
22
+ * produce an empty slug, so a short sha1 hash stands in — same fallback the
23
+ * v1 store used (ADR `2026-09-25-cjk-slug-hash-fallback`).
24
+ */
25
+ export declare function slugifyId(title: string): string;
26
+ /** Whether `id` is a well-formed store id. */
27
+ export declare function isValidMemoryId(id: string): boolean;
28
+ /** Labels of the feedback body lines that are missing, empty string when complete. */
29
+ export declare function feedbackWhyHowMissing(body: string): string;
30
+ /** One proposed mutation — the closed proposal shape both write paths speak. */
31
+ export interface MemoryProposal {
32
+ action: "create" | "update" | "delete";
33
+ /** Required for update/delete; optional for create (derived from title). */
34
+ id?: string | undefined;
35
+ type?: string | undefined;
36
+ title?: string | undefined;
37
+ description?: string | undefined;
38
+ body?: string | undefined;
39
+ /** Trajectory provenance for extraction writes ("820-831"). */
40
+ sourceSeqs?: string | undefined;
41
+ }
42
+ /** The merged field values a create/update would write (delete checks none). */
43
+ export interface ProposedFields {
44
+ type: MemoryType;
45
+ title: string;
46
+ description: string;
47
+ body: string;
48
+ }
49
+ /**
50
+ * Gates that depend only on the merged field values (not on store state).
51
+ * Returns the blocking reasons, empty when the record would pass. Runs the
52
+ * secret screen over the full visible text so a secret smuggled into any
53
+ * field blocks the write.
54
+ */
55
+ export declare function fieldGateReasons(fields: ProposedFields): string[];
56
+ //# sourceMappingURL=memory-rules.d.ts.map
@@ -0,0 +1,152 @@
1
+ /**
2
+ * The mechanical write gates every memory passes before the store accepts it
3
+ * — the "模型提议,代码保证" boundary made literal. Both write paths (the
4
+ * explicit `memory_write` tool and the proposal-based extraction) funnel
5
+ * through these checks; nothing reaches the database without them.
6
+ *
7
+ * Type taxonomy and the boundary judgment (what deserves a memory at all)
8
+ * live in the extraction prompt (`memory-guide.ts`); this module only holds
9
+ * what code can decide without judgment: enumerations, sizes, id shape, the
10
+ * feedback body contract, and secret screening.
11
+ */
12
+ import { createHash } from "node:crypto";
13
+ /** Memory types a record can carry (the guide's taxonomy, enforced). */
14
+ export const MEMORY_TYPES = ["user", "feedback", "reference"];
15
+ /** Record lifecycle statuses. `quarantined` is set by patrol, never shown. */
16
+ export const MEMORY_STATUSES = ["active", "quarantined", "archived"];
17
+ /** Hard body size cap: the one unbounded-growth entry point, killed at write. */
18
+ export const MEMORY_BODY_LIMIT = 65_536;
19
+ /** Sanity caps for the injection-facing fields (hook text, not prose). */
20
+ export const MEMORY_TITLE_LIMIT = 200;
21
+ export const MEMORY_DESCRIPTION_LIMIT = 500;
22
+ /** Kebab-slug id shape: lowercase alphanumerics joined by single dashes. */
23
+ const ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
24
+ /** Full id bound (slug + hash-fallback suffix headroom). */
25
+ export const MEMORY_ID_LIMIT = 80;
26
+ /**
27
+ * Fixed secret-detection patterns — a security invariant, deliberately NOT
28
+ * configurable: a user pattern typo must never be able to disable secret
29
+ * screening. Patterns carried over verbatim from the v1 promotion gate
30
+ * (ADR `2026-08-28-secret-leak-guard`), tuned for low false positives —
31
+ * placeholders like "YOUR_API_KEY_HERE" don't match; realistic mixed
32
+ * literals do.
33
+ */
34
+ const SECRET_PATTERNS = [
35
+ { label: "AnySearch API key", regex: /\bas_sk_[A-Za-z0-9]{8,}\b/ },
36
+ { label: "Anthropic API key", regex: /\bsk-ant-[A-Za-z0-9_-]{16,}\b/ },
37
+ { label: "OpenAI-style API key", regex: /\bsk-(?:proj-)?[A-Za-z0-9_-]{16,}\b/ },
38
+ { label: "GitHub token", regex: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}\b/ },
39
+ { label: "GitHub fine-grained PAT", regex: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/ },
40
+ { label: "AWS access key", regex: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/ },
41
+ { label: "Google API key", regex: /\bAIza[0-9A-Za-z_-]{35}\b/ },
42
+ { label: "Slack token", regex: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/ },
43
+ { label: "npm grant token", regex: /\bnpm_[A-Za-z0-9]{36}\b/ },
44
+ { label: "private key block", regex: /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY(?: BLOCK)?-----/ },
45
+ {
46
+ label: "credential assignment",
47
+ // The credential-name group is manually case-insensitive ("apiKey" in
48
+ // JSON, "API_KEY" in env examples) so the placeholder lookahead stays
49
+ // case-sensitive: it excludes only ALL-CAPS placeholders
50
+ // ("YOUR_KEY_HERE"), not real mixed/lowercase literals.
51
+ regex: /\b(?:[Aa][Pp][Ii][_-]?[Kk][Ee][Yy]|[Ss][Ee][Cc][Rr][Ee][Tt]|[Tt][Oo][Kk][Ee][Nn]|[Pp][Aa][Ss][Ss][Ww][Oo][Rr][Dd]|[Pp][Aa][Ss][Ss][Ww][Dd]|[Pp][Ww][Dd])\b["']?\s*[:=]\s*["'](?!["']*[A-Z0-9_]+["'])[A-Za-z0-9+/_-]{16,}["']/,
52
+ },
53
+ ];
54
+ /** Redact a matched secret for error/audit surfaces: family, never the value. */
55
+ function redactSecret(matched) {
56
+ if (matched.length <= 10) {
57
+ return `${matched.slice(0, 3)}…`;
58
+ }
59
+ return `${matched.slice(0, 6)}…${matched.slice(-3)}`;
60
+ }
61
+ /**
62
+ * First reason the content reads as carrying a live credential, or undefined
63
+ * when it screens clean. Applied to the whole proposed record (title +
64
+ * description + body) at write time, and again by patrol over stored rows.
65
+ */
66
+ export function secretLeakReason(content) {
67
+ for (const { label, regex } of SECRET_PATTERNS) {
68
+ const match = regex.exec(content);
69
+ if (match?.[0]) {
70
+ return `possible ${label} ("${redactSecret(match[0])}") — secrets must never be sedimented into the memory store; rotate the credential and keep it out of the store`;
71
+ }
72
+ }
73
+ return undefined;
74
+ }
75
+ /**
76
+ * Derive a kebab-slug id from a title. CJK (or any non-ASCII-only) titles
77
+ * produce an empty slug, so a short sha1 hash stands in — same fallback the
78
+ * v1 store used (ADR `2026-09-25-cjk-slug-hash-fallback`).
79
+ */
80
+ export function slugifyId(title) {
81
+ const slug = title
82
+ .toLowerCase()
83
+ .normalize("NFKD")
84
+ .replace(/[^a-z0-9]+/g, "-")
85
+ .replace(/^-+|-+$/g, "")
86
+ .slice(0, MEMORY_ID_LIMIT)
87
+ .replace(/-+$/g, "");
88
+ if (slug.length > 0) {
89
+ return slug;
90
+ }
91
+ return `m-${createHash("sha1").update(title).digest("hex").slice(0, 8)}`;
92
+ }
93
+ /** Whether `id` is a well-formed store id. */
94
+ export function isValidMemoryId(id) {
95
+ return id.length <= MEMORY_ID_LIMIT && ID_PATTERN.test(id);
96
+ }
97
+ /** The feedback body contract: rule first, then **Why:** and **How to apply:**. */
98
+ const FEEDBACK_WHY = /(?:\*\*)?Why:(?:\*\*)?/i;
99
+ const FEEDBACK_HOW = /(?:\*\*)?How to apply:(?:\*\*)?/i;
100
+ /** Labels of the feedback body lines that are missing, empty string when complete. */
101
+ export function feedbackWhyHowMissing(body) {
102
+ const missing = [];
103
+ if (!FEEDBACK_WHY.test(body)) {
104
+ missing.push("Why:");
105
+ }
106
+ if (!FEEDBACK_HOW.test(body)) {
107
+ missing.push("How to apply:");
108
+ }
109
+ return missing.join("、");
110
+ }
111
+ /**
112
+ * Gates that depend only on the merged field values (not on store state).
113
+ * Returns the blocking reasons, empty when the record would pass. Runs the
114
+ * secret screen over the full visible text so a secret smuggled into any
115
+ * field blocks the write.
116
+ */
117
+ export function fieldGateReasons(fields) {
118
+ const reasons = [];
119
+ if (!MEMORY_TYPES.includes(fields.type)) {
120
+ reasons.push(`type must be one of ${MEMORY_TYPES.join("/")}`);
121
+ }
122
+ if (fields.title.trim().length === 0) {
123
+ reasons.push("title must not be empty");
124
+ }
125
+ else if (fields.title.length > MEMORY_TITLE_LIMIT) {
126
+ reasons.push(`title exceeds ${MEMORY_TITLE_LIMIT} characters`);
127
+ }
128
+ if (fields.description.trim().length === 0) {
129
+ reasons.push("description must not be empty — it is the injection hook");
130
+ }
131
+ else if (fields.description.length > MEMORY_DESCRIPTION_LIMIT) {
132
+ reasons.push(`description exceeds ${MEMORY_DESCRIPTION_LIMIT} characters`);
133
+ }
134
+ if (fields.body.trim().length === 0) {
135
+ reasons.push("body must not be empty");
136
+ }
137
+ else if (fields.body.length > MEMORY_BODY_LIMIT) {
138
+ reasons.push(`body exceeds the ${MEMORY_BODY_LIMIT}-character cap`);
139
+ }
140
+ if (fields.type === "feedback") {
141
+ const missing = feedbackWhyHowMissing(fields.body);
142
+ if (missing.length > 0) {
143
+ reasons.push(`feedback body must carry the ${missing} lines`);
144
+ }
145
+ }
146
+ const leak = secretLeakReason(`${fields.title}\n${fields.description}\n${fields.body}`);
147
+ if (leak !== undefined) {
148
+ reasons.push(leak);
149
+ }
150
+ return reasons;
151
+ }
152
+ //# sourceMappingURL=memory-rules.js.map