@fonlan/dsh-gitmemo 0.1.5 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -65
- package/README.zh.md +55 -61
- package/cordis.patch.yml +10 -3
- package/lib/cli.d.ts +2 -0
- package/lib/cli.js +178 -0
- package/lib/index.d.ts +42 -38
- package/lib/index.js +243 -154
- package/lib/mem.d.ts +287 -65
- package/lib/mem.js +1475 -469
- package/lib/migrate.d.ts +95 -0
- package/lib/migrate.js +650 -0
- package/package.json +13 -7
package/lib/index.js
CHANGED
|
@@ -2,34 +2,43 @@
|
|
|
2
2
|
* dsh-gitmemo — Git-backed long-term memory for DeepSeek Harness.
|
|
3
3
|
*
|
|
4
4
|
* A Cordis plugin mirroring gitmemo (https://github.com/fonlan/gitmemo):
|
|
5
|
-
* the agent stores completed task outcomes as markdown
|
|
6
|
-
* `.mem` git repository
|
|
5
|
+
* the root agent stores completed task outcomes as immutable markdown
|
|
6
|
+
* entries in a local `.mem` git repository (single `main` branch, structured
|
|
7
|
+
* commit messages) and searches them before starting new work.
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
* -
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* Mechanism:
|
|
10
|
+
* - only ROOT agents (delegationDepth === 0) receive the gitmemo workflow
|
|
11
|
+
* section and the five `mem_*` tools, scoped into `agent.ctx` at
|
|
12
|
+
* `agent/created`; subagents carry neither (no token cost, no duplicate
|
|
13
|
+
* memory writes);
|
|
14
|
+
* - registration performs zero Git I/O;
|
|
15
|
+
* - no session-start seeding, no synchronous git runners, no branch
|
|
16
|
+
* alignment — `.mem` permanently stays on `main`;
|
|
17
|
+
* - the previous `mem_init` tool is gone from the model surface; legacy
|
|
18
|
+
* argument shapes are accepted for one version by the core engine
|
|
19
|
+
* compatibility adapter, while the model-facing schemas stay strict.
|
|
13
20
|
*
|
|
14
21
|
* @module dsh-gitmemo
|
|
15
22
|
*/
|
|
16
23
|
import z from "@deepseek-ai/schemastery";
|
|
17
24
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
18
|
-
import { GitMemo } from "./mem.js";
|
|
25
|
+
import { GitMemo, GitMemoError, resolveProjectRoot } from "./mem.js";
|
|
19
26
|
/** Cordis plugin name. */
|
|
20
27
|
const name = "dsh-gitmemo";
|
|
21
28
|
/** Host services this plugin needs. */
|
|
22
29
|
const inject = ["tools", "systemPrompt"];
|
|
23
|
-
/** Plugin configuration
|
|
30
|
+
/** Plugin configuration. */
|
|
24
31
|
const Config = z.object({
|
|
25
|
-
/**
|
|
32
|
+
/** @deprecated — the memory dir is fixed at `<projectRoot>/.mem`; kept for one version. */
|
|
26
33
|
memDirName: z.string().default(".mem"),
|
|
27
|
-
/** Max search hits per mem_search call. Default 20. */
|
|
34
|
+
/** Max search hits per mem_search call (page size). Default 20. */
|
|
28
35
|
searchLimit: z.number().default(20),
|
|
29
|
-
/**
|
|
36
|
+
/** @deprecated — `.mem` always stays on main; kept for one version, no effect. */
|
|
30
37
|
branchAlign: z.boolean().default(true),
|
|
31
|
-
/**
|
|
38
|
+
/** @deprecated — session-start recent seeding was removed; kept for one version, no effect. */
|
|
32
39
|
recentContextLimit: z.number().default(5),
|
|
40
|
+
/** Cross-process lock wait timeout in milliseconds. Default 30000. */
|
|
41
|
+
lockTimeoutMs: z.number().default(30000),
|
|
33
42
|
/** Optional explicit project root; defaults to the calling session's cwd. */
|
|
34
43
|
projectRoot: z.string()
|
|
35
44
|
});
|
|
@@ -37,14 +46,13 @@ const Config = z.object({
|
|
|
37
46
|
function sessionCwd(exec) {
|
|
38
47
|
return exec.agent?.session?.header?.cwd;
|
|
39
48
|
}
|
|
40
|
-
/** Build the engine for one tool call. */
|
|
41
|
-
function engineFor(exec, config) {
|
|
49
|
+
/** Build the engine for one tool call (project root per plan 3.1). */
|
|
50
|
+
async function engineFor(exec, config) {
|
|
42
51
|
const cwd = sessionCwd(exec) ?? process.cwd();
|
|
43
|
-
const root = config.projectRoot ?? cwd;
|
|
52
|
+
const root = config.projectRoot ?? (await resolveProjectRoot(cwd));
|
|
44
53
|
const engineConfig = {
|
|
45
|
-
memDirName: config.memDirName,
|
|
46
54
|
searchLimit: config.searchLimit,
|
|
47
|
-
|
|
55
|
+
lockTimeoutMs: config.lockTimeoutMs
|
|
48
56
|
};
|
|
49
57
|
return new GitMemo(root, engineConfig);
|
|
50
58
|
}
|
|
@@ -53,60 +61,83 @@ const SEARCH_HIT = {
|
|
|
53
61
|
additionalProperties: false,
|
|
54
62
|
properties: {
|
|
55
63
|
hash: { type: "string", required: true, description: "Full commit hash of the memory entry." },
|
|
56
|
-
title: { type: "string", required: true, description: "
|
|
57
|
-
date: { type: "string", required: true, description: "Commit date in ISO format." }
|
|
64
|
+
title: { type: "string", required: true, description: "Entry title (commit subject)." },
|
|
65
|
+
date: { type: "string", required: true, description: "Commit date in ISO format." },
|
|
66
|
+
summary: { type: "string", required: true, description: "Entry summary (from the commit body)." },
|
|
67
|
+
keywords: {
|
|
68
|
+
type: "array",
|
|
69
|
+
required: true,
|
|
70
|
+
items: { type: "string" },
|
|
71
|
+
description: "Structured keywords stored with the entry."
|
|
72
|
+
},
|
|
73
|
+
score: { type: "number", required: true, description: "Number of distinct query keywords matched." },
|
|
74
|
+
matched_keywords: {
|
|
75
|
+
type: "array",
|
|
76
|
+
required: true,
|
|
77
|
+
items: { type: "string" },
|
|
78
|
+
description: "The query keywords that actually matched this entry."
|
|
79
|
+
}
|
|
58
80
|
}
|
|
59
81
|
};
|
|
82
|
+
const KEYWORDS_ARRAY = {
|
|
83
|
+
type: "array",
|
|
84
|
+
items: { type: "string" },
|
|
85
|
+
description: "1-12 keywords (include 中英文 synonyms when useful)."
|
|
86
|
+
};
|
|
87
|
+
const RELATED_BRANCHES = {
|
|
88
|
+
type: "array",
|
|
89
|
+
items: { type: "string" },
|
|
90
|
+
description: "Related code branches (max 32; the current branch is always recorded)."
|
|
91
|
+
};
|
|
92
|
+
const RELATED_PATHS = {
|
|
93
|
+
type: "array",
|
|
94
|
+
items: { type: "string" },
|
|
95
|
+
description: "Project-relative paths (max 128; absolute paths and escaping paths are rejected)."
|
|
96
|
+
};
|
|
60
97
|
/** Render one tool output block as plain text. */
|
|
61
98
|
function textBlock(text) {
|
|
62
99
|
return [{ type: "text", text }];
|
|
63
100
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
101
|
+
function formatSearchResults(value) {
|
|
102
|
+
const lines = [
|
|
103
|
+
`snapshot=${value.snapshot} total=${value.total} next_skip=${value.next_skip ?? "null"}` +
|
|
104
|
+
(value.legacy === true ? " legacy=true" : "")
|
|
105
|
+
];
|
|
106
|
+
for (const hit of value.results) {
|
|
107
|
+
lines.push(hit.hash +
|
|
108
|
+
"|" +
|
|
109
|
+
hit.title +
|
|
110
|
+
"|" +
|
|
111
|
+
hit.date +
|
|
112
|
+
"|score=" +
|
|
113
|
+
hit.score +
|
|
114
|
+
"|matched=" +
|
|
115
|
+
hit.matched_keywords.join(","));
|
|
116
|
+
if (hit.summary.length > 0)
|
|
117
|
+
lines.push(" summary: " + hit.summary.replace(/\s+/g, " ").slice(0, 300));
|
|
118
|
+
}
|
|
119
|
+
if (value.results.length === 0)
|
|
120
|
+
lines.push("(no matching memories)");
|
|
121
|
+
if (value.warning !== undefined)
|
|
122
|
+
lines.push("warning: " + value.warning);
|
|
123
|
+
for (const diagnostic of value.diagnostics ?? [])
|
|
124
|
+
lines.push("diagnostic: " + diagnostic);
|
|
125
|
+
return lines.join("\n");
|
|
67
126
|
}
|
|
68
|
-
/** Register
|
|
127
|
+
/** Register the five model-facing tools into a scoped agent context. */
|
|
69
128
|
function registerMemTools(ctx, config) {
|
|
70
|
-
void config;
|
|
71
|
-
ctx.tools.register(defineTool({
|
|
72
|
-
name: "mem_init",
|
|
73
|
-
description: "Initialize the gitmemo long-term memory repository (.mem git repo) at the project root. All other mem_* tools auto-initialize, so this is only needed to check or force initialization.",
|
|
74
|
-
parameters: {},
|
|
75
|
-
output: {
|
|
76
|
-
schema: {
|
|
77
|
-
type: "object",
|
|
78
|
-
additionalProperties: false,
|
|
79
|
-
properties: {
|
|
80
|
-
path: { type: "string", required: true, description: "Absolute path of the memory repository." }
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
|
-
render: (_args, value) => textBlock("OK: Memory repo ready at " + value.path)
|
|
84
|
-
},
|
|
85
|
-
isConcurrencySafe: () => false,
|
|
86
|
-
async execute(_args, exec) {
|
|
87
|
-
const memo = engineFor(exec, config);
|
|
88
|
-
const path = await memo.init();
|
|
89
|
-
return { path };
|
|
90
|
-
},
|
|
91
|
-
presentCall: () => ({ card: "generic", title: "Init gitmemo memory", kind: "other" })
|
|
92
|
-
}));
|
|
93
129
|
ctx.tools.register(defineTool({
|
|
94
130
|
name: "mem_search",
|
|
95
|
-
description: "Search the gitmemo long-term memory (.mem git repo) for past task outcomes. Call BEFORE starting work: extract
|
|
131
|
+
description: "Search the gitmemo long-term memory (.mem git repo) for past task outcomes. Call BEFORE starting repo-related work: extract 1-12 中英文关键词 from the user request (include synonyms in both languages when useful) and run mem_search; pure chat and general Q&A need no search. Returns a snapshot, total, next_skip and up to 20 scored hits with summary/keywords/matched_keywords — select at most 5 most relevant hits and mem_read them. Paginate by passing skip and the returned snapshot back unchanged; a stale snapshot is rejected explicitly.",
|
|
96
132
|
parameters: {
|
|
97
|
-
keywords: {
|
|
98
|
-
type: "string",
|
|
99
|
-
required: true,
|
|
100
|
-
description: "Comma-separated keywords (3-5 recommended), e.g. \"auth,rate-limit,login\"."
|
|
101
|
-
},
|
|
133
|
+
keywords: { ...KEYWORDS_ARRAY, required: true },
|
|
102
134
|
skip: {
|
|
103
135
|
type: "integer",
|
|
104
136
|
description: "Pagination offset (default 0)."
|
|
105
137
|
},
|
|
106
|
-
|
|
138
|
+
snapshot: {
|
|
107
139
|
type: "string",
|
|
108
|
-
|
|
109
|
-
description: "and=strict (all keywords), or=broad (any keyword), auto=AND first then OR fallback (default auto)."
|
|
140
|
+
description: "Snapshot returned by the first page — pass it back unchanged for stable pagination."
|
|
110
141
|
}
|
|
111
142
|
},
|
|
112
143
|
output: {
|
|
@@ -114,34 +145,48 @@ function registerMemTools(ctx, config) {
|
|
|
114
145
|
type: "object",
|
|
115
146
|
additionalProperties: false,
|
|
116
147
|
properties: {
|
|
117
|
-
|
|
118
|
-
|
|
148
|
+
snapshot: { type: "string", required: true, description: "Stable search snapshot; reuse for pagination." },
|
|
149
|
+
total: { type: "number", required: true, description: "Total matching results in the snapshot." },
|
|
150
|
+
next_skip: {
|
|
151
|
+
oneOf: [{ type: "number" }, { type: "null" }],
|
|
152
|
+
required: true,
|
|
153
|
+
description: "Offset for the next page, or null."
|
|
154
|
+
},
|
|
155
|
+
results: { type: "array", required: true, items: SEARCH_HIT },
|
|
156
|
+
legacy: { type: "boolean", description: "True when searching a legacy (pre-migration) repo read-only." },
|
|
157
|
+
warning: { type: "string", description: "Compatibility/deprecation warning, when present." },
|
|
158
|
+
diagnostics: {
|
|
159
|
+
type: "array",
|
|
160
|
+
items: { type: "string" },
|
|
161
|
+
description: "Malformed commit/file mappings skipped during search."
|
|
162
|
+
}
|
|
119
163
|
}
|
|
120
164
|
},
|
|
121
|
-
render: (_args, value) =>
|
|
122
|
-
const lines = value.results.map((hit) => hit.hash + "|" + hit.title + "|" + hit.date);
|
|
123
|
-
return textBlock(lines.length > 0 ? lines.join("\n") : "(no matching memories)");
|
|
124
|
-
}
|
|
165
|
+
render: (_args, value) => textBlock(formatSearchResults(value))
|
|
125
166
|
},
|
|
126
167
|
isConcurrencySafe: () => true,
|
|
127
168
|
async execute(args, exec) {
|
|
128
|
-
const memo = engineFor(exec, config);
|
|
129
|
-
return await memo.search(args.keywords,
|
|
169
|
+
const memo = await engineFor(exec, config);
|
|
170
|
+
return await memo.search(args.keywords, {
|
|
171
|
+
skip: args.skip ?? 0,
|
|
172
|
+
snapshot: args.snapshot,
|
|
173
|
+
mode: args.mode
|
|
174
|
+
});
|
|
130
175
|
},
|
|
131
176
|
presentCall: (args) => ({
|
|
132
177
|
card: "generic",
|
|
133
|
-
title: "Search memory: " + args.keywords,
|
|
178
|
+
title: "Search memory: " + (Array.isArray(args.keywords) ? args.keywords.join(", ") : args.keywords),
|
|
134
179
|
kind: "other"
|
|
135
180
|
})
|
|
136
181
|
}));
|
|
137
182
|
ctx.tools.register(defineTool({
|
|
138
183
|
name: "mem_read",
|
|
139
|
-
description: "Read one gitmemo memory entry by commit hash (returned by mem_search). Select only the most relevant memories before reading — at most 5 when a search returns more.",
|
|
184
|
+
description: "Read one gitmemo memory entry by commit hash (returned by mem_search). Select only the most relevant memories before reading — at most 5 when a search returns more. Historical entries that were replaced or deleted stay readable for audit.",
|
|
140
185
|
parameters: {
|
|
141
186
|
commit_hash: {
|
|
142
187
|
type: "string",
|
|
143
188
|
required: true,
|
|
144
|
-
description: "Commit hash of the memory entry to read."
|
|
189
|
+
description: "Commit hash of the memory entry to read (create or replace hash)."
|
|
145
190
|
}
|
|
146
191
|
},
|
|
147
192
|
output: {
|
|
@@ -151,16 +196,17 @@ function registerMemTools(ctx, config) {
|
|
|
151
196
|
properties: {
|
|
152
197
|
commit_hash: { type: "string", required: true },
|
|
153
198
|
file: { type: "string", required: true, description: "Entry file path inside the memory repo." },
|
|
154
|
-
content: { type: "string", required: true, description: "Full markdown of the memory entry." }
|
|
199
|
+
content: { type: "string", required: true, description: "Full markdown of the memory entry." },
|
|
200
|
+
legacy: { type: "boolean", description: "True for legacy-format commits (pre-migration)." }
|
|
155
201
|
}
|
|
156
202
|
},
|
|
157
|
-
render: (_args, value) => textBlock(value.content)
|
|
203
|
+
render: (_args, value) => textBlock((value.legacy === true ? "[legacy entry]\n" : "") + value.content)
|
|
158
204
|
},
|
|
159
205
|
isConcurrencySafe: () => true,
|
|
160
206
|
async execute(args, exec) {
|
|
161
|
-
const memo = engineFor(exec, config);
|
|
162
|
-
const { file, content } = await memo.read(args.commit_hash);
|
|
163
|
-
return { commit_hash:
|
|
207
|
+
const memo = await engineFor(exec, config);
|
|
208
|
+
const { hash, file, content, legacy } = await memo.read(args.commit_hash);
|
|
209
|
+
return { commit_hash: hash, file, content, ...(legacy ? { legacy: true } : {}) };
|
|
164
210
|
},
|
|
165
211
|
presentCall: (args) => ({
|
|
166
212
|
card: "generic",
|
|
@@ -170,29 +216,30 @@ function registerMemTools(ctx, config) {
|
|
|
170
216
|
}));
|
|
171
217
|
ctx.tools.register(defineTool({
|
|
172
218
|
name: "mem_write",
|
|
173
|
-
description: "Store a completed task outcome in the gitmemo long-term memory (.mem git repo). Write ONLY when the task is complete AND repo-related AND the outcome is valuable/reusable OR the user explicitly asked to remember.
|
|
219
|
+
description: "Store a completed task outcome in the gitmemo long-term memory (.mem git repo). Write ONLY when the task is complete AND repo-related AND the outcome is valuable/reusable OR the user explicitly asked to remember. Entries are immutable: title (single line, ≤200 chars, \"[module] action + object\"), summary (≤4000 chars), keywords (2-12, include 中英文同义词) and content (markdown body; the engine generates front matter) are all required; related_branches/related_paths are optional. Corrections use mem_replace, withdrawal uses mem_delete — never rewrite in place.",
|
|
174
220
|
parameters: {
|
|
175
221
|
title: {
|
|
176
222
|
type: "string",
|
|
177
223
|
required: true,
|
|
178
|
-
description: "Short \"[module] action + object\" title, e.g. \"[auth] add rate-limit for login\"."
|
|
224
|
+
description: "Short \"[module] action + object\" title, single line, e.g. \"[auth] add rate-limit for login\"."
|
|
179
225
|
},
|
|
180
|
-
|
|
226
|
+
summary: {
|
|
181
227
|
type: "string",
|
|
182
|
-
|
|
228
|
+
required: true,
|
|
229
|
+
description: "1-3 sentence summary of the outcome (multi-line allowed, ≤4000 chars)."
|
|
183
230
|
},
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
231
|
+
keywords: {
|
|
232
|
+
...KEYWORDS_ARRAY,
|
|
233
|
+
required: true,
|
|
234
|
+
description: "2-12 keywords (1-64 chars each); include 中英文同义词, e.g. [\"auth\", \"rate-limit\", \"登录\", \"限流\"]."
|
|
187
235
|
},
|
|
188
|
-
|
|
236
|
+
content: {
|
|
189
237
|
type: "string",
|
|
190
|
-
|
|
238
|
+
required: true,
|
|
239
|
+
description: "The memory markdown body (engine generates front matter and standard sections)."
|
|
191
240
|
},
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
description: "Optional commit body: 1-3 sentence summary + metadata, passed inline. Not memory content."
|
|
195
|
-
}
|
|
241
|
+
related_branches: RELATED_BRANCHES,
|
|
242
|
+
related_paths: RELATED_PATHS
|
|
196
243
|
},
|
|
197
244
|
output: {
|
|
198
245
|
schema: {
|
|
@@ -200,25 +247,25 @@ function registerMemTools(ctx, config) {
|
|
|
200
247
|
additionalProperties: false,
|
|
201
248
|
properties: {
|
|
202
249
|
hash: { type: "string", required: true, description: "Commit hash of the new memory entry." },
|
|
203
|
-
file: { type: "string", required: true, description: "Entry file path inside the memory repo." }
|
|
250
|
+
file: { type: "string", required: true, description: "Entry file path inside the memory repo." },
|
|
251
|
+
legacy: { type: "boolean", description: "True when written through the legacy compatibility adapter." }
|
|
204
252
|
}
|
|
205
253
|
},
|
|
206
|
-
render: (_args, value) => textBlock("OK: " + value.hash + "|" + value.file)
|
|
254
|
+
render: (_args, value) => textBlock("OK: " + value.hash + "|" + value.file + (value.legacy === true ? " (legacy adapter)" : ""))
|
|
207
255
|
},
|
|
208
256
|
isConcurrencySafe: () => false,
|
|
209
|
-
async execute(
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const memo = engineFor(exec, config);
|
|
257
|
+
async execute(args, exec) {
|
|
258
|
+
const memo = await engineFor(exec, config);
|
|
259
|
+
if (args.file !== undefined) {
|
|
260
|
+
throw new GitMemoError("gitmemo: mem_write `file` (in-place commit) is not supported by the immutable entry model — pass content directly, or use mem_replace to update an entry");
|
|
261
|
+
}
|
|
215
262
|
return await memo.write({
|
|
216
263
|
title: args.title,
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
264
|
+
summary: args.summary ?? "",
|
|
265
|
+
keywords: args.keywords ?? [],
|
|
266
|
+
content: args.content ?? "",
|
|
267
|
+
related_branches: args.related_branches,
|
|
268
|
+
related_paths: args.related_paths
|
|
222
269
|
});
|
|
223
270
|
},
|
|
224
271
|
presentCall: (args) => ({
|
|
@@ -229,12 +276,17 @@ function registerMemTools(ctx, config) {
|
|
|
229
276
|
}));
|
|
230
277
|
ctx.tools.register(defineTool({
|
|
231
278
|
name: "mem_delete",
|
|
232
|
-
description: "
|
|
279
|
+
description: "Withdraw a gitmemo memory entry whose conclusion is obsolete and has NO replacement. Requires the active commit hash (returned by mem_search) and a required reason. Use mem_replace when a new conclusion replaces the old one.",
|
|
233
280
|
parameters: {
|
|
234
281
|
commit_hash: {
|
|
235
282
|
type: "string",
|
|
236
283
|
required: true,
|
|
237
|
-
description: "
|
|
284
|
+
description: "Active commit hash of the memory entry to withdraw."
|
|
285
|
+
},
|
|
286
|
+
reason: {
|
|
287
|
+
type: "string",
|
|
288
|
+
required: true,
|
|
289
|
+
description: "Why the conclusion is withdrawn (recorded in the delete commit)."
|
|
238
290
|
}
|
|
239
291
|
},
|
|
240
292
|
output: {
|
|
@@ -249,8 +301,8 @@ function registerMemTools(ctx, config) {
|
|
|
249
301
|
},
|
|
250
302
|
isConcurrencySafe: () => false,
|
|
251
303
|
async execute(args, exec) {
|
|
252
|
-
const memo = engineFor(exec, config);
|
|
253
|
-
const file = await memo.delete(args.commit_hash);
|
|
304
|
+
const memo = await engineFor(exec, config);
|
|
305
|
+
const { file } = await memo.delete({ commit_hash: args.commit_hash, reason: args.reason });
|
|
254
306
|
return { file };
|
|
255
307
|
},
|
|
256
308
|
presentCall: (args) => ({
|
|
@@ -259,75 +311,112 @@ function registerMemTools(ctx, config) {
|
|
|
259
311
|
kind: "other"
|
|
260
312
|
})
|
|
261
313
|
}));
|
|
314
|
+
ctx.tools.register(defineTool({
|
|
315
|
+
name: "mem_replace",
|
|
316
|
+
description: "Replace an active gitmemo memory entry with a new conclusion in ONE atomic commit (deletes the old entry file, adds the new one). Use when the user corrects a stored outcome — never delete-then-write. commit_hash must be the ACTIVE hash returned by mem_search; stale hashes are rejected. Takes the same fields as mem_write.",
|
|
317
|
+
parameters: {
|
|
318
|
+
commit_hash: {
|
|
319
|
+
type: "string",
|
|
320
|
+
required: true,
|
|
321
|
+
description: "Active commit hash of the entry to replace."
|
|
322
|
+
},
|
|
323
|
+
title: {
|
|
324
|
+
type: "string",
|
|
325
|
+
required: true,
|
|
326
|
+
description: "Short \"[module] action + object\" title, single line."
|
|
327
|
+
},
|
|
328
|
+
summary: {
|
|
329
|
+
type: "string",
|
|
330
|
+
required: true,
|
|
331
|
+
description: "1-3 sentence summary of the new conclusion."
|
|
332
|
+
},
|
|
333
|
+
keywords: {
|
|
334
|
+
...KEYWORDS_ARRAY,
|
|
335
|
+
required: true,
|
|
336
|
+
description: "2-12 keywords (1-64 chars each); include 中英文同义词."
|
|
337
|
+
},
|
|
338
|
+
content: {
|
|
339
|
+
type: "string",
|
|
340
|
+
required: true,
|
|
341
|
+
description: "The new memory markdown body."
|
|
342
|
+
},
|
|
343
|
+
related_branches: RELATED_BRANCHES,
|
|
344
|
+
related_paths: RELATED_PATHS
|
|
345
|
+
},
|
|
346
|
+
output: {
|
|
347
|
+
schema: {
|
|
348
|
+
type: "object",
|
|
349
|
+
additionalProperties: false,
|
|
350
|
+
properties: {
|
|
351
|
+
hash: { type: "string", required: true, description: "Commit hash of the replace commit." },
|
|
352
|
+
file: { type: "string", required: true, description: "New entry file path inside the memory repo." }
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
render: (_args, value) => textBlock("OK: " + value.hash + "|" + value.file)
|
|
356
|
+
},
|
|
357
|
+
isConcurrencySafe: () => false,
|
|
358
|
+
async execute(args, exec) {
|
|
359
|
+
const memo = await engineFor(exec, config);
|
|
360
|
+
return await memo.replace({
|
|
361
|
+
commit_hash: args.commit_hash,
|
|
362
|
+
title: args.title,
|
|
363
|
+
summary: args.summary ?? "",
|
|
364
|
+
keywords: args.keywords ?? [],
|
|
365
|
+
content: args.content ?? "",
|
|
366
|
+
related_branches: args.related_branches,
|
|
367
|
+
related_paths: args.related_paths
|
|
368
|
+
});
|
|
369
|
+
},
|
|
370
|
+
presentCall: (args) => ({
|
|
371
|
+
card: "generic",
|
|
372
|
+
title: "Replace memory " + args.commit_hash.slice(0, 8) + ": " + args.title,
|
|
373
|
+
kind: "other"
|
|
374
|
+
})
|
|
375
|
+
}));
|
|
262
376
|
}
|
|
263
377
|
/**
|
|
264
|
-
* Register the
|
|
265
|
-
* agents-template.md). This section is part of every session's system prompt,
|
|
266
|
-
* so the rules cannot be missed; the mem_* tool descriptions carry the
|
|
267
|
-
* argument contract for each operation.
|
|
378
|
+
* Register the gitmemo workflow rules (plan 9.2) into a scoped agent context.
|
|
268
379
|
*/
|
|
269
380
|
function registerPromptSection(ctx) {
|
|
270
381
|
ctx.systemPrompt.section({
|
|
271
382
|
name: "memory:gitmemo",
|
|
272
383
|
order: 150,
|
|
273
384
|
text: [
|
|
274
|
-
"This deployment provides gitmemo long-term memory: a local .mem git repository at the project root stores past task outcomes as markdown entries; git is the only dependency. Use the dedicated tools only (mem_search / mem_read / mem_write / mem_delete — all auto-initialize); never shell out to git or read .mem files directly.",
|
|
275
|
-
"- BEFORE WORK — search:
|
|
276
|
-
"-
|
|
277
|
-
"- END-OF-SESSION CHECKPOINT — the ONLY write path:
|
|
385
|
+
"This deployment provides gitmemo long-term memory: a local .mem git repository at the project root stores past task outcomes as immutable markdown entries; git is the only dependency. Use the dedicated tools only (mem_search / mem_read / mem_write / mem_delete / mem_replace — all auto-initialize, and legacy-format .mem repos migrate automatically on first use); never shell out to git or read .mem files directly.",
|
|
386
|
+
"- BEFORE WORK — search: 仓库相关任务开始前提取 1–12 个中英文关键词调用 mem_search(含中英文同义词);纯闲聊和通用问答无需搜索。",
|
|
387
|
+
"- RESULT PRESELECT — 根据 title、summary、keywords、score 和 matched keywords 选择最多 5 条执行 mem_read,复用相关结论;无相关结果时用返回的 snapshot + skip 翻页。",
|
|
388
|
+
"- END-OF-SESSION CHECKPOINT — the ONLY write path: 会话结束时回顾全程,仅当结论有价值、可复用或用户明确要求记住时 mem_write;纯问答、未完成任务、非仓库工作、纯操作类 git 动作(仅 commit/push)不写;避免语义重复。",
|
|
389
|
+
"- USER CORRECTION — 用户更正已有结论:有替代结论时调用 mem_replace(不得先 delete 再 write);结论作废且无替代时调用带 reason 的 mem_delete。",
|
|
390
|
+
"- SUBAGENT RESULTS — 子代理结果由根 Agent 汇总后决定是否形成一条会话级记忆。"
|
|
278
391
|
].join("\n")
|
|
279
392
|
});
|
|
280
393
|
}
|
|
281
394
|
/**
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
* is read-only (no .mem auto-init), and failures degrade to a log warning.
|
|
286
|
-
*/
|
|
287
|
-
function registerRecentContext(ctx, config) {
|
|
288
|
-
if (config.recentContextLimit <= 0)
|
|
289
|
-
return;
|
|
290
|
-
ctx.on("agent/created", ({ agent }) => {
|
|
291
|
-
const header = agent.session?.header;
|
|
292
|
-
if (header?.cwd === undefined || (header.delegationDepth ?? 0) > 0)
|
|
293
|
-
return;
|
|
294
|
-
try {
|
|
295
|
-
// Synchronous scan: registers the context before the first request assembles.
|
|
296
|
-
const memo = new GitMemo(config.projectRoot ?? header.cwd, {
|
|
297
|
-
memDirName: config.memDirName,
|
|
298
|
-
searchLimit: config.searchLimit,
|
|
299
|
-
branchAlign: config.branchAlign
|
|
300
|
-
});
|
|
301
|
-
const hits = memo.recentSync(config.recentContextLimit);
|
|
302
|
-
if (hits.length === 0)
|
|
303
|
-
return;
|
|
304
|
-
const lines = hits.map((hit) => hit.hash + "|" + hit.title + "|" + hit.date);
|
|
305
|
-
agent.ctx.systemPrompt.context({
|
|
306
|
-
name: "memory:gitmemo-recent",
|
|
307
|
-
order: 400,
|
|
308
|
-
text: "Recent gitmemo memories from previous sessions (mem_read <hash> for details, mem_search for targeted lookups):\n" + lines.join("\n")
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
catch (error) {
|
|
312
|
-
ctx.logger.warn(`dsh-gitmemo: recent-memory injection skipped for agent "${agent.id}": ${String(error)}`);
|
|
313
|
-
}
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
/**
|
|
317
|
-
* Register the plugin: tools, always-on rules, and session-start seed.
|
|
318
|
-
* @param ctx - registrant context carrying the host tool/prompt services.
|
|
319
|
-
* @param config - plugin configuration (defaults applied by the loader).
|
|
395
|
+
* Register the plugin: root-agent-scoped mem tools + workflow rules. No
|
|
396
|
+
* global tool registration, no session-start seeding, no Git I/O at
|
|
397
|
+
* registration time.
|
|
320
398
|
*/
|
|
321
399
|
async function apply(ctx, config = {}) {
|
|
322
400
|
const resolved = {
|
|
323
|
-
memDirName: config.memDirName ?? ".mem",
|
|
324
401
|
searchLimit: config.searchLimit ?? 20,
|
|
325
|
-
|
|
326
|
-
recentContextLimit: config.recentContextLimit ?? 5,
|
|
402
|
+
lockTimeoutMs: config.lockTimeoutMs ?? 30000,
|
|
327
403
|
projectRoot: config.projectRoot
|
|
328
404
|
};
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
405
|
+
if (config.branchAlign !== undefined && config.branchAlign !== true) {
|
|
406
|
+
ctx.logger.warn("dsh-gitmemo: `branchAlign` is deprecated and has no effect — .mem always stays on main");
|
|
407
|
+
}
|
|
408
|
+
if (config.recentContextLimit !== undefined && config.recentContextLimit !== 5) {
|
|
409
|
+
ctx.logger.warn("dsh-gitmemo: `recentContextLimit` is deprecated and has no effect — session-start recent seeding was removed");
|
|
410
|
+
}
|
|
411
|
+
if (config.memDirName !== undefined && config.memDirName !== ".mem") {
|
|
412
|
+
ctx.logger.warn("dsh-gitmemo: `memDirName` is deprecated — the memory directory is fixed at <projectRoot>/.mem; pass a legacy dir as the migration --source");
|
|
413
|
+
}
|
|
414
|
+
ctx.on("agent/created", ({ agent }) => {
|
|
415
|
+
const header = agent.session?.header;
|
|
416
|
+
if ((header?.delegationDepth ?? 0) > 0)
|
|
417
|
+
return; // subagents: no gitmemo workflow or tools
|
|
418
|
+
registerMemTools(agent.ctx, resolved);
|
|
419
|
+
registerPromptSection(agent.ctx);
|
|
420
|
+
});
|
|
332
421
|
}
|
|
333
422
|
export { Config, apply, inject, name };
|