dsh-continual-evolve 0.14.2 → 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.
- package/README.md +33 -21
- package/README.zh.md +33 -21
- 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 +60 -0
- package/lib/extraction.js +269 -0
- package/lib/import-md.d.ts +15 -0
- package/lib/import-md.js +140 -0
- package/lib/index.d.ts +63 -27
- package/lib/index.js +121 -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 +95 -0
- package/lib/store.js +207 -0
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +2 -2
package/lib/index.js
CHANGED
|
@@ -1,46 +1,60 @@
|
|
|
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, 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
|
|
21
|
-
*
|
|
22
|
-
* the
|
|
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
|
import { mountCardRoutes } from "./card-routes.js";
|
|
26
|
-
import {
|
|
30
|
+
import { createExtractionScheduler } from "./extraction.js";
|
|
27
31
|
import { createFrozenMemorySection, cwdOf, DEFAULT_MAX_CHARS, DEFAULT_MEMORY_SECTION_ORDER, MEMORY_SECTION_NAME, memorySectionText, } from "./memory-section.js";
|
|
28
|
-
import {
|
|
32
|
+
import { registerMemoryTools } from "./memory-tools.js";
|
|
33
|
+
import { importWorkspaceMd } from "./import-md.js";
|
|
34
|
+
import { openMemoryStore } from "./store.js";
|
|
29
35
|
export const name = "continual-evolve";
|
|
30
36
|
export const inject = ["systemPrompt"];
|
|
31
37
|
export const Config = z.object({
|
|
32
38
|
/**
|
|
33
|
-
* The memory section: injects the workspace memory index
|
|
34
|
-
* start, frozen per session to keep the system prompt
|
|
35
|
-
* (prompt cache). `enabled`/`guide`/`order`/`maxChars`
|
|
36
|
-
* true/true/400/6000; with `guide` off and an empty store the
|
|
37
|
-
* renders to "" and costs no tokens.
|
|
39
|
+
* The memory section: injects the workspace memory index (a store query)
|
|
40
|
+
* at session start, frozen per session to keep the system prompt
|
|
41
|
+
* byte-stable (prompt cache). `enabled`/`guide`/`order`/`maxChars`
|
|
42
|
+
* default to true/true/400/6000; with `guide` off and an empty store the
|
|
43
|
+
* section renders to "" and costs no tokens.
|
|
38
44
|
*/
|
|
39
45
|
memoryIndex: z.object({
|
|
40
46
|
enabled: z.boolean(),
|
|
41
47
|
guide: z.boolean(),
|
|
42
48
|
order: z.natural(),
|
|
43
49
|
maxChars: z.natural(),
|
|
50
|
+
/**
|
|
51
|
+
* The proposal-based extraction run (trigger: turn-level with idle
|
|
52
|
+
* debounce; writes land only through the store's gates). Off keeps
|
|
53
|
+
* the plugin injection + explicit tools only.
|
|
54
|
+
*/
|
|
55
|
+
extraction: z.boolean(),
|
|
56
|
+
/** Idle debounce minutes between turn end and an extraction run (0 = per turn). */
|
|
57
|
+
debounceMin: z.natural(),
|
|
44
58
|
}),
|
|
45
59
|
/**
|
|
46
60
|
* The read-only memory card in the official plugin manager
|
|
@@ -52,15 +66,60 @@ export const Config = z.object({
|
|
|
52
66
|
enabled: z.boolean(),
|
|
53
67
|
}),
|
|
54
68
|
});
|
|
69
|
+
/** Register the memory tools through a nested inject (see memory-tools.ts). */
|
|
70
|
+
function mountToolsWhenAvailable(host, store, state) {
|
|
71
|
+
const nested = host;
|
|
72
|
+
if (typeof nested.inject !== "function") {
|
|
73
|
+
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)");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
nested.inject(["tools"], (scoped) => {
|
|
77
|
+
const tools = scoped.tools;
|
|
78
|
+
if (tools === undefined || typeof tools.register !== "function") {
|
|
79
|
+
host.logger("continual-evolve").warn("tools service present but empty — memory tools not registered (injection unaffected)");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
registerMemoryTools(tools, store);
|
|
83
|
+
state.toolsAvailable = true;
|
|
84
|
+
host.logger("continual-evolve").info("memory_write and memory_read registered");
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Arm the extraction scheduler through a nested inject: the session reader
|
|
89
|
+
* and LLM services are siblings, so they are resolved inside the scoped
|
|
90
|
+
* context while the event listeners use the plugin's own context. A host
|
|
91
|
+
* without either service (or without `.on`) simply never arms — injection
|
|
92
|
+
* and the explicit tools remain functional.
|
|
93
|
+
*/
|
|
94
|
+
function mountExtractionWhenAvailable(host, store, config) {
|
|
95
|
+
const log = host.logger("continual-evolve");
|
|
96
|
+
if (config.memoryIndex?.extraction === false) {
|
|
97
|
+
log.info("extraction disabled by config — injection and memory_write remain active");
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const nested = host;
|
|
101
|
+
const events = host;
|
|
102
|
+
if (typeof nested.inject !== "function" || typeof events.on !== "function") {
|
|
103
|
+
log.warn("host context exposes no inject/on — extraction not armed (injection and tools unaffected)");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
nested.inject(["sessionQuery", "llm"], (scoped) => {
|
|
107
|
+
const context = scoped;
|
|
108
|
+
const surface = context.sessionQuery;
|
|
109
|
+
const llm = (context.llm ?? context.get?.("llm"));
|
|
110
|
+
if (surface === undefined || typeof surface.readSurface !== "function") {
|
|
111
|
+
log.warn("sessionQuery service present but empty — extraction not armed (injection and tools unaffected)");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
createExtractionScheduler(events, { store, surface, llm }, { debounceMin: config.memoryIndex?.debounceMin ?? 10 });
|
|
115
|
+
});
|
|
116
|
+
}
|
|
55
117
|
/**
|
|
56
118
|
* 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
|
|
58
|
-
*
|
|
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.
|
|
119
|
+
* the plugin stays mountable on hosts without that service. Called once the
|
|
120
|
+
* store is ready — the routes are projections of the store, not of files.
|
|
62
121
|
*/
|
|
63
|
-
function mountCardWhenAvailable(host,
|
|
122
|
+
function mountCardWhenAvailable(host, store) {
|
|
64
123
|
const nested = host;
|
|
65
124
|
if (typeof nested.inject !== "function") {
|
|
66
125
|
host.logger("continual-evolve").warn("host context exposes no inject — memory card routes not mounted (memory section unaffected)");
|
|
@@ -72,60 +131,57 @@ function mountCardWhenAvailable(host, served) {
|
|
|
72
131
|
host.logger("continual-evolve").warn("webServer service present but empty — memory card routes not mounted (memory section unaffected)");
|
|
73
132
|
return;
|
|
74
133
|
}
|
|
75
|
-
|
|
76
|
-
registry: registryLookup(scoped),
|
|
77
|
-
served: served,
|
|
78
|
-
homeDir: process.env.DSH_HOME,
|
|
79
|
-
});
|
|
80
|
-
mountCardRoutes(webServer, catalog);
|
|
134
|
+
mountCardRoutes(webServer, store);
|
|
81
135
|
});
|
|
82
136
|
}
|
|
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
137
|
export function apply(host, config) {
|
|
106
138
|
if (config.memoryIndex?.enabled === false) {
|
|
107
139
|
return;
|
|
108
140
|
}
|
|
109
|
-
const
|
|
141
|
+
const log = host.logger("continual-evolve");
|
|
142
|
+
const state = { store: undefined, toolsAvailable: false };
|
|
143
|
+
void openMemoryStore().then((store) => {
|
|
144
|
+
state.store = store;
|
|
145
|
+
mountToolsWhenAvailable(host, store, state);
|
|
146
|
+
mountExtractionWhenAvailable(host, store, config);
|
|
147
|
+
if (config.memoryCard?.enabled !== false) {
|
|
148
|
+
mountCardWhenAvailable(host, store);
|
|
149
|
+
}
|
|
150
|
+
}, (error) => {
|
|
151
|
+
// node:sqlite missing (old runtime) or the database unusable:
|
|
152
|
+
// degrade to a no-op plugin, never take the host down.
|
|
153
|
+
log.warn(`memory store unavailable — injection and tools disabled: ${error instanceof Error ? error.message : String(error)}`);
|
|
154
|
+
});
|
|
110
155
|
const frozen = createFrozenMemorySection();
|
|
111
156
|
host.systemPrompt.section({
|
|
112
157
|
name: MEMORY_SECTION_NAME,
|
|
113
158
|
order: config.memoryIndex?.order ?? DEFAULT_MEMORY_SECTION_ORDER,
|
|
114
159
|
text: (context) => {
|
|
160
|
+
const store = state.store;
|
|
161
|
+
if (store === undefined) {
|
|
162
|
+
// Still loading or unavailable: render nothing (and freeze
|
|
163
|
+
// nothing — the first assembly after readiness builds for real).
|
|
164
|
+
return "";
|
|
165
|
+
}
|
|
115
166
|
const cwd = cwdOf(context.agent);
|
|
116
167
|
if (cwd !== undefined) {
|
|
117
|
-
|
|
168
|
+
// First contact with this workspace migrates any legacy MD-era
|
|
169
|
+
// store into the database; flag-guarded, so it is a no-op after.
|
|
170
|
+
try {
|
|
171
|
+
importWorkspaceMd(store, cwd);
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
log.warn(`legacy memory import failed for ${cwd}: ${error instanceof Error ? error.message : String(error)}`);
|
|
175
|
+
}
|
|
118
176
|
}
|
|
119
177
|
return frozen.textFor(context.agent, (agent) => memorySectionText(agent, {
|
|
178
|
+
store,
|
|
120
179
|
maxChars: config.memoryIndex?.maxChars ?? DEFAULT_MAX_CHARS,
|
|
121
180
|
guide: config.memoryIndex?.guide ?? true,
|
|
181
|
+
toolsAvailable: state.toolsAvailable,
|
|
122
182
|
}));
|
|
123
183
|
},
|
|
124
184
|
});
|
|
125
|
-
|
|
126
|
-
if (config.memoryCard?.enabled === false) {
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
mountCardWhenAvailable(host, workspaces);
|
|
185
|
+
log.info("continual-evolve memory section registered (central SQLite store, sole-writer gates)");
|
|
130
186
|
}
|
|
131
187
|
//# sourceMappingURL=index.js.map
|
package/lib/memory-guide.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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
|
package/lib/memory-guide.js
CHANGED
|
@@ -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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|