minimal-agent 0.18.8 → 0.18.10
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/package.json +1 -1
- package/src/loop.js +35 -5
- package/src/loop.ts +35 -5
- package/src/memory/factLearning.js +3 -3
- package/src/memory/factLearning.ts +3 -7
- package/src/memory/jobPolicy.js +21 -13
- package/src/memory/jobPolicy.ts +22 -11
- package/src/memory/learning.js +3 -3
- package/src/memory/learning.ts +3 -3
- package/src/memory/learningModel.js +4 -2
- package/src/memory/learningModel.ts +4 -2
- package/src/prompts/delegation.js +15 -0
- package/src/prompts/delegation.ts +20 -0
- package/src/prompts/policyRegistry.js +3 -2
- package/src/prompts/policyRegistry.ts +3 -2
- package/src/tools/agent/agent.js +2 -1
- package/src/tools/agent/agent.ts +2 -1
- package/src/tools/applypatch/applyPatch.js +1 -1
- package/src/tools/applypatch/applyPatch.ts +1 -1
- package/src/tools/edit/edit.js +11 -15
- package/src/tools/edit/edit.ts +10 -15
- package/src/tools/edit/multi-edit.js +14 -4
- package/src/tools/edit/multi-edit.ts +10 -4
- package/src/tools/memory/maintenance.js +2 -0
- package/src/tools/memory/maintenance.ts +2 -0
- package/src/tools/memory/memory.js +58 -14
- package/src/tools/memory/memory.ts +57 -13
- package/src/tools/memory/policy.js +16 -1
- package/src/tools/memory/policy.ts +16 -1
- package/src/tools/read/read.js +91 -47
- package/src/tools/read/read.ts +77 -46
- package/src/tools/shared/diffSummary.js +25 -3
- package/src/tools/shared/diffSummary.ts +23 -3
- package/src/tools/shared/editTransaction.js +45 -0
- package/src/tools/shared/editTransaction.ts +39 -0
- package/src/tools/shared/fileActivity.js +10 -2
- package/src/tools/shared/fileActivity.ts +11 -2
- package/src/tools/shared/fileState.js +2 -0
- package/src/tools/shared/fileState.ts +4 -0
- package/src/tools/shared/readBudget.js +12 -0
- package/src/tools/shared/readBudget.ts +12 -0
- package/src/tools/toolPromptGuides.js +6 -5
- package/src/tools/toolPromptGuides.ts +6 -5
- package/src/tools/types.js +2 -6
- package/src/tools/types.ts +2 -6
- package/src/types.ts +4 -2
package/src/tools/edit/edit.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* v0.2 增强:
|
|
9
9
|
* - 路径安全校验(null byte / UNC 路径)
|
|
10
|
-
* -
|
|
10
|
+
* - 共享有界编辑事务与提交后回读核验
|
|
11
11
|
* - old_string 找不到时提供模糊匹配提示(最常见模型错误)
|
|
12
12
|
* ============================================================
|
|
13
13
|
*/
|
|
@@ -15,14 +15,13 @@ import { existsSync } from 'node:fs';
|
|
|
15
15
|
import { z } from 'zod';
|
|
16
16
|
import { DEFAULT_MAX_RESULT_SIZE_CHARS } from '../types.js';
|
|
17
17
|
import { toToolParameters } from '../../utils/zodToJson.js';
|
|
18
|
-
import { resolveToolPath, findActualString, preserveQuoteStyle, detectLineEndingsForString, applyLineEnding, countOccurrences, splitReplaceAll, tryAutoSnapshot, atomicWriteFile, encodeTextFile,
|
|
18
|
+
import { resolveToolPath, findActualString, preserveQuoteStyle, detectLineEndingsForString, applyLineEnding, countOccurrences, splitReplaceAll, tryAutoSnapshot, atomicWriteFile, encodeTextFile, } from '../shared/fileUtils.js';
|
|
19
19
|
import { filePathField } from '../shared/schemas.js';
|
|
20
20
|
import { assertFresh, hashFileBytes, recordRead, wasTruncated } from '../shared/fileState.js';
|
|
21
21
|
import { fileActivityMetadata } from '../shared/fileActivity.js';
|
|
22
22
|
import { emitFileMutation } from '../shared/fileMutationBus.js';
|
|
23
23
|
import { buildPatchSummary } from '../shared/diffSummary.js';
|
|
24
|
-
|
|
25
|
-
const MAX_EDIT_FILE_SIZE_BYTES = 1024 * 1024 * 1024;
|
|
24
|
+
import { readEditSnapshot, commitEdit } from '../shared/editTransaction.js';
|
|
26
25
|
// ---------------- 1. Zod 输入 schema ----------------
|
|
27
26
|
const inputSchema = z.object({
|
|
28
27
|
file_path: filePathField('编辑'),
|
|
@@ -43,7 +42,7 @@ const description = `Performs exact string replacements in files.
|
|
|
43
42
|
Usage:
|
|
44
43
|
- You MUST use your \`Read\` tool to read the current content of the file BEFORE calling Edit.
|
|
45
44
|
Memory is unreliable — if you think you "remember" how the code looks, you are probably wrong.
|
|
46
|
-
|
|
45
|
+
Read the relevant sections; reuse unchanged read evidence instead of repeatedly reading the whole file.
|
|
47
46
|
- To create a new file, pass an empty \`old_string\` and the desired contents as \`new_string\`. When creating a new file (path does not yet exist), you may call Edit directly — no prior Read is needed.
|
|
48
47
|
- If the file already exists and you pass an empty \`old_string\`, the edit is rejected with a clear error. To replace the entire content of an existing file, use the Write tool instead.
|
|
49
48
|
- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
|
|
@@ -116,9 +115,11 @@ async function call(input, state, cwd) {
|
|
|
116
115
|
};
|
|
117
116
|
}
|
|
118
117
|
let original;
|
|
118
|
+
let editSnapshot;
|
|
119
119
|
let sourceEncoding;
|
|
120
120
|
try {
|
|
121
|
-
const snapshot = await
|
|
121
|
+
const snapshot = await readEditSnapshot(filePath);
|
|
122
|
+
editSnapshot = snapshot;
|
|
122
123
|
original = snapshot.content;
|
|
123
124
|
sourceEncoding = snapshot.encoding;
|
|
124
125
|
}
|
|
@@ -127,14 +128,6 @@ async function call(input, state, cwd) {
|
|
|
127
128
|
}
|
|
128
129
|
// 5d-1. 记录原文件行尾符风格(写入时保持一致)
|
|
129
130
|
const originalLineEnding = detectLineEndingsForString(original);
|
|
130
|
-
// 5d. 文件大小上限检查
|
|
131
|
-
const fileSize = Buffer.byteLength(original, 'utf8');
|
|
132
|
-
if (fileSize > MAX_EDIT_FILE_SIZE_BYTES) {
|
|
133
|
-
return {
|
|
134
|
-
ok: false,
|
|
135
|
-
error: `文件过大(${fileSize} 字节 > ${MAX_EDIT_FILE_SIZE_BYTES} 字节 ≈ 1GB)。Edit 不适合操作超大文件,请用 Write 工具。`,
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
131
|
// 唯性检查
|
|
139
132
|
if (old_string === '') {
|
|
140
133
|
return {
|
|
@@ -176,7 +169,7 @@ async function call(input, state, cwd) {
|
|
|
176
169
|
// 5f. 保持原文件行尾符风格
|
|
177
170
|
const normalizedReplaced = applyLineEnding(replaced, originalLineEnding);
|
|
178
171
|
try {
|
|
179
|
-
await
|
|
172
|
+
await commitEdit(filePath, editSnapshot, normalizedReplaced);
|
|
180
173
|
recordRead(filePath, {
|
|
181
174
|
contentHash: hashFileBytes(encodeTextFile(normalizedReplaced, sourceEncoding)),
|
|
182
175
|
}, state);
|
|
@@ -193,6 +186,7 @@ async function call(input, state, cwd) {
|
|
|
193
186
|
ok: true,
|
|
194
187
|
content: `已编辑 ${filePath}\n` +
|
|
195
188
|
`替换次数:${replaceAll ? occurrences : 1}\n` +
|
|
189
|
+
`已回读核验提交内容(不代表代码测试通过)。\n` +
|
|
196
190
|
`行数:${linesBefore} → ${linesAfter}\n` + (patch.unifiedDiff ?? '[diff omitted: too large]'),
|
|
197
191
|
metadata: await fileActivityMetadata(filePath, 'modified', 'not_run', 'support', cwd, patch),
|
|
198
192
|
};
|
|
@@ -208,6 +202,8 @@ async function call(input, state, cwd) {
|
|
|
208
202
|
* 3. 返回重叠率最高的前 N 行作为提示
|
|
209
203
|
*/
|
|
210
204
|
function findFuzzyMatchHint(fileContent, target) {
|
|
205
|
+
if (fileContent.length > 200_000 || target.length > 2_000)
|
|
206
|
+
return '大文件不执行全文件模糊匹配;用 Grep 定位,再 Read 目标范围获取精确文本。';
|
|
211
207
|
const MIN_OVERLAP_RATIO = 0.5; // 至少 50% 字符重叠才认为"接近"
|
|
212
208
|
const MAX_HINTS = 3; // 最多提示 3 个候选
|
|
213
209
|
const targetTrimmed = target.trim();
|
package/src/tools/edit/edit.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* v0.2 增强:
|
|
9
9
|
* - 路径安全校验(null byte / UNC 路径)
|
|
10
|
-
* -
|
|
10
|
+
* - 共享有界编辑事务与提交后回读核验
|
|
11
11
|
* - old_string 找不到时提供模糊匹配提示(最常见模型错误)
|
|
12
12
|
* ============================================================
|
|
13
13
|
*/
|
|
@@ -36,9 +36,8 @@ import { assertFresh, hashFileBytes, recordRead, wasTruncated, type FileReadStat
|
|
|
36
36
|
import { fileActivityMetadata } from '../shared/fileActivity.js';
|
|
37
37
|
import { emitFileMutation } from '../shared/fileMutationBus.js';
|
|
38
38
|
import { buildPatchSummary } from '../shared/diffSummary.js';
|
|
39
|
+
import { readEditSnapshot, commitEdit } from '../shared/editTransaction.js';
|
|
39
40
|
|
|
40
|
-
/** 编辑文件大小上限:1GB。防止模型试图 Edit 超大文件撑爆内存。 */
|
|
41
|
-
const MAX_EDIT_FILE_SIZE_BYTES = 1024 * 1024 * 1024;
|
|
42
41
|
|
|
43
42
|
// ---------------- 1. Zod 输入 schema ----------------
|
|
44
43
|
const inputSchema = z.object({
|
|
@@ -65,7 +64,7 @@ const description = `Performs exact string replacements in files.
|
|
|
65
64
|
Usage:
|
|
66
65
|
- You MUST use your \`Read\` tool to read the current content of the file BEFORE calling Edit.
|
|
67
66
|
Memory is unreliable — if you think you "remember" how the code looks, you are probably wrong.
|
|
68
|
-
|
|
67
|
+
Read the relevant sections; reuse unchanged read evidence instead of repeatedly reading the whole file.
|
|
69
68
|
- To create a new file, pass an empty \`old_string\` and the desired contents as \`new_string\`. When creating a new file (path does not yet exist), you may call Edit directly — no prior Read is needed.
|
|
70
69
|
- If the file already exists and you pass an empty \`old_string\`, the edit is rejected with a clear error. To replace the entire content of an existing file, use the Write tool instead.
|
|
71
70
|
- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
|
|
@@ -145,9 +144,11 @@ async function call(input: EditInput, state?: FileReadState, cwd?: string): Prom
|
|
|
145
144
|
}
|
|
146
145
|
|
|
147
146
|
let original: string;
|
|
147
|
+
let editSnapshot: Awaited<ReturnType<typeof readEditSnapshot>>;
|
|
148
148
|
let sourceEncoding: Awaited<ReturnType<typeof readTextFile>>['encoding'];
|
|
149
149
|
try {
|
|
150
|
-
const snapshot = await
|
|
150
|
+
const snapshot = await readEditSnapshot(filePath);
|
|
151
|
+
editSnapshot = snapshot;
|
|
151
152
|
original = snapshot.content;
|
|
152
153
|
sourceEncoding = snapshot.encoding;
|
|
153
154
|
} catch (e) {
|
|
@@ -157,15 +158,6 @@ async function call(input: EditInput, state?: FileReadState, cwd?: string): Prom
|
|
|
157
158
|
// 5d-1. 记录原文件行尾符风格(写入时保持一致)
|
|
158
159
|
const originalLineEnding = detectLineEndingsForString(original);
|
|
159
160
|
|
|
160
|
-
// 5d. 文件大小上限检查
|
|
161
|
-
const fileSize = Buffer.byteLength(original, 'utf8');
|
|
162
|
-
if (fileSize > MAX_EDIT_FILE_SIZE_BYTES) {
|
|
163
|
-
return {
|
|
164
|
-
ok: false,
|
|
165
|
-
error: `文件过大(${fileSize} 字节 > ${MAX_EDIT_FILE_SIZE_BYTES} 字节 ≈ 1GB)。Edit 不适合操作超大文件,请用 Write 工具。`,
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
|
|
169
161
|
// 唯性检查
|
|
170
162
|
if (old_string === '') {
|
|
171
163
|
return {
|
|
@@ -212,7 +204,7 @@ async function call(input: EditInput, state?: FileReadState, cwd?: string): Prom
|
|
|
212
204
|
const normalizedReplaced = applyLineEnding(replaced, originalLineEnding);
|
|
213
205
|
|
|
214
206
|
try {
|
|
215
|
-
await
|
|
207
|
+
await commitEdit(filePath, editSnapshot, normalizedReplaced);
|
|
216
208
|
recordRead(filePath, {
|
|
217
209
|
contentHash: hashFileBytes(encodeTextFile(normalizedReplaced, sourceEncoding)),
|
|
218
210
|
}, state);
|
|
@@ -230,6 +222,7 @@ async function call(input: EditInput, state?: FileReadState, cwd?: string): Prom
|
|
|
230
222
|
content:
|
|
231
223
|
`已编辑 ${filePath}\n` +
|
|
232
224
|
`替换次数:${replaceAll ? occurrences : 1}\n` +
|
|
225
|
+
`已回读核验提交内容(不代表代码测试通过)。\n` +
|
|
233
226
|
`行数:${linesBefore} → ${linesAfter}\n` + (patch.unifiedDiff ?? '[diff omitted: too large]'),
|
|
234
227
|
metadata: await fileActivityMetadata(filePath, 'modified', 'not_run', 'support', cwd, patch),
|
|
235
228
|
};
|
|
@@ -247,6 +240,8 @@ async function call(input: EditInput, state?: FileReadState, cwd?: string): Prom
|
|
|
247
240
|
* 3. 返回重叠率最高的前 N 行作为提示
|
|
248
241
|
*/
|
|
249
242
|
function findFuzzyMatchHint(fileContent: string, target: string): string | null {
|
|
243
|
+
if (fileContent.length > 200_000 || target.length > 2_000)
|
|
244
|
+
return '大文件不执行全文件模糊匹配;用 Grep 定位,再 Read 目标范围获取精确文本。';
|
|
250
245
|
const MIN_OVERLAP_RATIO = 0.5; // 至少 50% 字符重叠才认为"接近"
|
|
251
246
|
const MAX_HINTS = 3; // 最多提示 3 个候选
|
|
252
247
|
|
|
@@ -17,12 +17,13 @@ import { existsSync } from 'node:fs';
|
|
|
17
17
|
import { z } from 'zod';
|
|
18
18
|
import { DEFAULT_MAX_RESULT_SIZE_CHARS } from '../types.js';
|
|
19
19
|
import { toToolParameters } from '../../utils/zodToJson.js';
|
|
20
|
-
import { resolveToolPath, detectLineEndingsForString, applyLineEnding, findActualString, preserveQuoteStyle, countOccurrences, splitReplaceAll, tryAutoSnapshot,
|
|
20
|
+
import { resolveToolPath, detectLineEndingsForString, applyLineEnding, findActualString, preserveQuoteStyle, countOccurrences, splitReplaceAll, tryAutoSnapshot, encodeTextFile, } from '../shared/fileUtils.js';
|
|
21
21
|
import { filePathField } from '../shared/schemas.js';
|
|
22
22
|
import { assertFresh, hashFileBytes, recordRead, wasTruncated } from '../shared/fileState.js';
|
|
23
23
|
import { fileActivityMetadata } from '../shared/fileActivity.js';
|
|
24
24
|
import { emitFileMutation } from '../shared/fileMutationBus.js';
|
|
25
25
|
import { buildPatchSummary } from '../shared/diffSummary.js';
|
|
26
|
+
import { readEditSnapshot, commitEdit } from '../shared/editTransaction.js';
|
|
26
27
|
// ---------------- 1. Zod 输入 schema ----------------
|
|
27
28
|
const editItemSchema = z.object({
|
|
28
29
|
old_string: z.string().min(1).describe('要替换的原文本(不允许为空 —— 创建新文件请用 Edit 工具)'),
|
|
@@ -108,9 +109,11 @@ async function call(input, state, cwd) {
|
|
|
108
109
|
}
|
|
109
110
|
// 4e. 读原文
|
|
110
111
|
let originalContent;
|
|
112
|
+
let editSnapshot;
|
|
111
113
|
let sourceEncoding;
|
|
112
114
|
try {
|
|
113
|
-
const snapshot = await
|
|
115
|
+
const snapshot = await readEditSnapshot(filePath);
|
|
116
|
+
editSnapshot = snapshot;
|
|
114
117
|
originalContent = snapshot.content;
|
|
115
118
|
sourceEncoding = snapshot.encoding;
|
|
116
119
|
}
|
|
@@ -119,6 +122,8 @@ async function call(input, state, cwd) {
|
|
|
119
122
|
}
|
|
120
123
|
// 4f. 在内存中顺序应用所有 edit(任一失败立即返回,磁盘不动)
|
|
121
124
|
let currentContent = originalContent;
|
|
125
|
+
let replacements = 0;
|
|
126
|
+
let unchanged = 0;
|
|
122
127
|
const lineEnding = detectLineEndingsForString(originalContent);
|
|
123
128
|
const truncHint = wasTruncated(filePath, state)
|
|
124
129
|
? `\n\n⚠️ 上次 Read 该文件时被截断了(只看到部分内容)。当前 old_string 可能在截断范围外。请用更小的 limit + offset 精确读取目标行号附近后再 MultiEdit。`
|
|
@@ -153,6 +158,11 @@ async function call(input, state, cwd) {
|
|
|
153
158
|
error: `edits[${i}].old_string 在当前内容中出现 ${occurrences} 次,不唯一。请扩大 old_string 包含更多上下文,或显式传 replace_all=true。${truncHint}`,
|
|
154
159
|
};
|
|
155
160
|
}
|
|
161
|
+
if (searchTarget === processedNewString) {
|
|
162
|
+
unchanged++;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
replacements += replaceAll ? occurrences : 1;
|
|
156
166
|
currentContent = replaceAll
|
|
157
167
|
? splitReplaceAll(currentContent, searchTarget, processedNewString)
|
|
158
168
|
: currentContent.replace(searchTarget, () => processedNewString);
|
|
@@ -160,7 +170,7 @@ async function call(input, state, cwd) {
|
|
|
160
170
|
// 4g. 全部成功 → 保持原文件行尾符风格,写盘
|
|
161
171
|
const finalContent = applyLineEnding(currentContent, lineEnding);
|
|
162
172
|
try {
|
|
163
|
-
await
|
|
173
|
+
await commitEdit(filePath, editSnapshot, finalContent);
|
|
164
174
|
recordRead(filePath, {
|
|
165
175
|
contentHash: hashFileBytes(encodeTextFile(finalContent, sourceEncoding)),
|
|
166
176
|
}, state);
|
|
@@ -172,7 +182,7 @@ async function call(input, state, cwd) {
|
|
|
172
182
|
const patch = buildPatchSummary(originalContent, finalContent, input.file_path);
|
|
173
183
|
return {
|
|
174
184
|
ok: true,
|
|
175
|
-
content:
|
|
185
|
+
content: `已编辑 ${filePath}:${edits.length} 条操作,实际替换 ${replacements} 次,${unchanged} 条无变化。已回读核验提交内容。\n${patch.unifiedDiff ?? '[diff 未统计:文件过大;不代表无变化]'}`,
|
|
176
186
|
metadata: await fileActivityMetadata(filePath, 'modified', 'not_run', 'support', cwd, patch),
|
|
177
187
|
};
|
|
178
188
|
}
|
|
@@ -29,7 +29,6 @@ import {
|
|
|
29
29
|
countOccurrences,
|
|
30
30
|
splitReplaceAll,
|
|
31
31
|
tryAutoSnapshot,
|
|
32
|
-
atomicWriteFile,
|
|
33
32
|
encodeTextFile,
|
|
34
33
|
readTextFile,
|
|
35
34
|
} from '../shared/fileUtils.js';
|
|
@@ -38,6 +37,7 @@ import { assertFresh, hashFileBytes, recordRead, wasTruncated, type FileReadStat
|
|
|
38
37
|
import { fileActivityMetadata } from '../shared/fileActivity.js';
|
|
39
38
|
import { emitFileMutation } from '../shared/fileMutationBus.js';
|
|
40
39
|
import { buildPatchSummary } from '../shared/diffSummary.js';
|
|
40
|
+
import { readEditSnapshot, commitEdit } from '../shared/editTransaction.js';
|
|
41
41
|
|
|
42
42
|
// ---------------- 1. Zod 输入 schema ----------------
|
|
43
43
|
const editItemSchema = z.object({
|
|
@@ -134,9 +134,11 @@ async function call(input: MultiEditInput, state?: FileReadState, cwd?: string):
|
|
|
134
134
|
|
|
135
135
|
// 4e. 读原文
|
|
136
136
|
let originalContent: string;
|
|
137
|
+
let editSnapshot: Awaited<ReturnType<typeof readEditSnapshot>>;
|
|
137
138
|
let sourceEncoding: Awaited<ReturnType<typeof readTextFile>>['encoding'];
|
|
138
139
|
try {
|
|
139
|
-
const snapshot = await
|
|
140
|
+
const snapshot = await readEditSnapshot(filePath);
|
|
141
|
+
editSnapshot = snapshot;
|
|
140
142
|
originalContent = snapshot.content;
|
|
141
143
|
sourceEncoding = snapshot.encoding;
|
|
142
144
|
} catch (e) {
|
|
@@ -145,6 +147,8 @@ async function call(input: MultiEditInput, state?: FileReadState, cwd?: string):
|
|
|
145
147
|
|
|
146
148
|
// 4f. 在内存中顺序应用所有 edit(任一失败立即返回,磁盘不动)
|
|
147
149
|
let currentContent = originalContent;
|
|
150
|
+
let replacements = 0;
|
|
151
|
+
let unchanged = 0;
|
|
148
152
|
const lineEnding = detectLineEndingsForString(originalContent);
|
|
149
153
|
const truncHint = wasTruncated(filePath, state)
|
|
150
154
|
? `\n\n⚠️ 上次 Read 该文件时被截断了(只看到部分内容)。当前 old_string 可能在截断范围外。请用更小的 limit + offset 精确读取目标行号附近后再 MultiEdit。`
|
|
@@ -182,6 +186,8 @@ async function call(input: MultiEditInput, state?: FileReadState, cwd?: string):
|
|
|
182
186
|
};
|
|
183
187
|
}
|
|
184
188
|
|
|
189
|
+
if (searchTarget === processedNewString) { unchanged++; continue; }
|
|
190
|
+
replacements += replaceAll ? occurrences : 1;
|
|
185
191
|
currentContent = replaceAll
|
|
186
192
|
? splitReplaceAll(currentContent, searchTarget, processedNewString)
|
|
187
193
|
: currentContent.replace(searchTarget, () => processedNewString);
|
|
@@ -191,7 +197,7 @@ async function call(input: MultiEditInput, state?: FileReadState, cwd?: string):
|
|
|
191
197
|
const finalContent = applyLineEnding(currentContent, lineEnding);
|
|
192
198
|
|
|
193
199
|
try {
|
|
194
|
-
await
|
|
200
|
+
await commitEdit(filePath, editSnapshot, finalContent);
|
|
195
201
|
recordRead(filePath, {
|
|
196
202
|
contentHash: hashFileBytes(encodeTextFile(finalContent, sourceEncoding)),
|
|
197
203
|
}, state);
|
|
@@ -203,7 +209,7 @@ async function call(input: MultiEditInput, state?: FileReadState, cwd?: string):
|
|
|
203
209
|
const patch = buildPatchSummary(originalContent, finalContent, input.file_path);
|
|
204
210
|
return {
|
|
205
211
|
ok: true,
|
|
206
|
-
content:
|
|
212
|
+
content: `已编辑 ${filePath}:${edits.length} 条操作,实际替换 ${replacements} 次,${unchanged} 条无变化。已回读核验提交内容。\n${patch.unifiedDiff ?? '[diff 未统计:文件过大;不代表无变化]'}`,
|
|
207
213
|
metadata: await fileActivityMetadata(filePath, 'modified', 'not_run', 'support', cwd, patch),
|
|
208
214
|
};
|
|
209
215
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { backgroundLearningHealth } from '../../memory/jobPolicy.js';
|
|
2
3
|
import { getWorkingDir } from '../../bootstrap/workingDir.js';
|
|
3
4
|
import { sha256 } from '../../memory/codec.js';
|
|
4
5
|
import { captureDependencies } from '../../memory/dependencies.js';
|
|
@@ -289,6 +290,7 @@ export const memoryMaintenanceTool = {
|
|
|
289
290
|
ok: true,
|
|
290
291
|
content: JSON.stringify({
|
|
291
292
|
...(await persistedLearningStatus(ctx.workspaceId)),
|
|
293
|
+
recentBackgroundFailures: backgroundLearningHealth(ctx.workspaceId),
|
|
292
294
|
revision: store.revision,
|
|
293
295
|
taskSummaries: {
|
|
294
296
|
count: store.taskSummaries?.length ?? 0,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { backgroundLearningHealth } from '../../memory/jobPolicy.js';
|
|
2
3
|
import { getWorkingDir } from '../../bootstrap/workingDir.js';
|
|
3
4
|
import { sha256 } from '../../memory/codec.js';
|
|
4
5
|
import { captureDependencies } from '../../memory/dependencies.js';
|
|
@@ -388,6 +389,7 @@ export const memoryMaintenanceTool: Tool<z.infer<typeof schema>> = {
|
|
|
388
389
|
content: JSON.stringify(
|
|
389
390
|
{
|
|
390
391
|
...(await persistedLearningStatus(ctx.workspaceId)),
|
|
392
|
+
recentBackgroundFailures: backgroundLearningHealth(ctx.workspaceId),
|
|
391
393
|
revision: store.revision,
|
|
392
394
|
taskSummaries: {
|
|
393
395
|
count: store.taskSummaries?.length ?? 0,
|
|
@@ -6,7 +6,7 @@ import { memoryRecordPath } from '../../memory/fileStore.js';
|
|
|
6
6
|
import { memoryService, } from '../../memory/memoryService.js';
|
|
7
7
|
import { toToolParameters } from '../../utils/zodToJson.js';
|
|
8
8
|
import { DEFAULT_MAX_RESULT_SIZE_CHARS } from '../types.js';
|
|
9
|
-
import { EXPLICIT_CROSS_WORKSPACE_INTENT, EXPLICIT_FORGET_INTENT, EXPLICIT_MEMORY_INTENT, EXPLICIT_PERSONA_INTENT, confirmSensitiveMemory, denyUnlessLocalOwner, requireCurrentEvidence, } from './policy.js';
|
|
9
|
+
import { EXPLICIT_CROSS_WORKSPACE_INTENT, EXPLICIT_FORGET_INTENT, EXPLICIT_MEMORY_INTENT, EXPLICIT_PERSONA_INTENT, confirmSensitiveMemory, confirmMemoryCorrection, denyUnlessLocalOwner, requireCurrentEvidence, } from './policy.js';
|
|
10
10
|
const scopeSchema = z.enum(['personal', 'workspace']);
|
|
11
11
|
const categorySchema = z.enum([
|
|
12
12
|
'profile',
|
|
@@ -21,7 +21,7 @@ function formatRecord(record, version) {
|
|
|
21
21
|
? ` workspace=${record.workspaceId}`
|
|
22
22
|
: '';
|
|
23
23
|
const lifecycle = ` status=${record.status ?? 'active'} revision=${record.revision ?? 1}`;
|
|
24
|
-
return `- [${record.id}] scope=${record.scope} ${record.category}/${record.subject}${workspace}${lifecycle}${version ? ` version=${version}` : ''}: ${record.statement}(${record.evidence},${new Date(record.updatedAt).toISOString()}
|
|
24
|
+
return `- [${record.id}] scope=${record.scope} ${record.category}/${record.subject}${workspace}${lifecycle}${version ? ` version=${version}` : ''}: ${record.statement}(${record.evidence},${new Date(record.updatedAt).toISOString()})${record.status === 'needs_review' && record.confirmationContext ? `\n复核线索(非事实):${record.confirmationContext}` : ''}`;
|
|
25
25
|
}
|
|
26
26
|
function diagnosticSuffix(diagnostics) {
|
|
27
27
|
if (!diagnostics.length)
|
|
@@ -159,15 +159,18 @@ export const rememberMemoryTool = {
|
|
|
159
159
|
},
|
|
160
160
|
};
|
|
161
161
|
const updateSchema = z.object({
|
|
162
|
+
action: z.enum(['update', 'review', 'confirm']).optional().describe('默认 update:当前用户原话纠正;review:仅标记待复核,不改事实;confirm:通过宿主向用户确认拟更新内容后保存'),
|
|
163
|
+
reason: z.string().min(1).max(1000).optional().describe('review/confirm 必填:冲突或纠正依据,注明来源;不得伪造用户陈述'),
|
|
162
164
|
id: z.string().uuid(),
|
|
163
165
|
scope: scopeSchema,
|
|
164
166
|
category: categorySchema.optional(),
|
|
165
167
|
subject: z.string().min(1).max(120).optional(),
|
|
166
|
-
statement: z.string().min(1).max(1000),
|
|
168
|
+
statement: z.string().min(1).max(1000).optional(),
|
|
167
169
|
evidenceQuote: z
|
|
168
170
|
.string()
|
|
169
171
|
.min(1)
|
|
170
172
|
.max(1000)
|
|
173
|
+
.optional()
|
|
171
174
|
.describe('当前用户消息中的逐字证据片段'),
|
|
172
175
|
sensitivity: z.enum(['normal', 'sensitive']).optional(),
|
|
173
176
|
aliases: z.array(z.string().min(1).max(120)).max(20).optional(),
|
|
@@ -184,7 +187,7 @@ const updateSchema = z.object({
|
|
|
184
187
|
});
|
|
185
188
|
export const updateMemoryTool = {
|
|
186
189
|
name: 'UpdateMemory',
|
|
187
|
-
description: '
|
|
190
|
+
description: '纠正用户事实记忆:先 RecallMemory 读取 ID/version。默认 update 需要 statement 与当前用户原话 evidenceQuote;review 需要 reason,仅标记待复核、保留原事实与证据;confirm 需要 statement/reason,通过宿主展示新旧内容并取得用户确认再更新。拒绝或无法交互不写入。项目知识和经验走 MemoryMaintenance,不伪造用户证据。',
|
|
188
191
|
inputSchema: updateSchema,
|
|
189
192
|
parameters: toToolParameters(updateSchema),
|
|
190
193
|
isReadOnly: false,
|
|
@@ -196,9 +199,23 @@ export const updateMemoryTool = {
|
|
|
196
199
|
if (denied)
|
|
197
200
|
return denied;
|
|
198
201
|
const current = ctx.currentUserInput ?? '';
|
|
199
|
-
const
|
|
200
|
-
if (
|
|
201
|
-
return
|
|
202
|
+
const action = input.action ?? 'update';
|
|
203
|
+
if (action === 'review' && (input.statement !== undefined || input.evidenceQuote !== undefined)) {
|
|
204
|
+
return { ok: false, error: 'review 仅提交 reason,不提交 statement/evidenceQuote;标记疑点不代表确认新事实' };
|
|
205
|
+
}
|
|
206
|
+
if (action !== 'update' && (input.category !== undefined || input.subject !== undefined || input.aliases !== undefined || input.supersedesIds !== undefined || input.sensitivity !== undefined)) {
|
|
207
|
+
return { ok: false, error: 'review/confirm 只处理当前记录的复核状态或正文,不同时修改分类、敏感级别、别名或替代其他记录' };
|
|
208
|
+
}
|
|
209
|
+
if (action === 'update') {
|
|
210
|
+
if (!input.statement || !input.evidenceQuote)
|
|
211
|
+
return { ok: false, error: 'update 需要 statement 和当前用户原话 evidenceQuote' };
|
|
212
|
+
const invalidEvidence = requireCurrentEvidence(ctx, input.evidenceQuote);
|
|
213
|
+
if (invalidEvidence)
|
|
214
|
+
return invalidEvidence;
|
|
215
|
+
}
|
|
216
|
+
else if (!input.reason || (action === 'confirm' && !input.statement)) {
|
|
217
|
+
return { ok: false, error: 'review 需要 reason;confirm 需要 reason 和 statement' };
|
|
218
|
+
}
|
|
202
219
|
if (input.scope === 'workspace' && !ctx.workspaceId) {
|
|
203
220
|
return {
|
|
204
221
|
ok: false,
|
|
@@ -206,7 +223,7 @@ export const updateMemoryTool = {
|
|
|
206
223
|
};
|
|
207
224
|
}
|
|
208
225
|
if (!(await memoryService.settings()).autoRemember &&
|
|
209
|
-
!EXPLICIT_MEMORY_INTENT.test(current)) {
|
|
226
|
+
!EXPLICIT_MEMORY_INTENT.test(current) && action !== 'confirm') {
|
|
210
227
|
return {
|
|
211
228
|
ok: false,
|
|
212
229
|
error: '用户已关闭自动记忆;当前消息没有明确要求更新记忆',
|
|
@@ -215,18 +232,43 @@ export const updateMemoryTool = {
|
|
|
215
232
|
const currentRecord = await memoryService.readWithVersion(input.id, input.scope, input.scope === 'workspace' ? ctx.workspaceId : undefined);
|
|
216
233
|
if (!currentRecord)
|
|
217
234
|
return { ok: false, error: '未找到该记忆,未做修改' };
|
|
218
|
-
const
|
|
235
|
+
const expectedVersion = input.expectedVersion ?? currentRecord.version;
|
|
236
|
+
if (expectedVersion !== currentRecord.version) {
|
|
237
|
+
return {
|
|
238
|
+
ok: false,
|
|
239
|
+
error: '记忆已被手工或并发修改。未应用本次更新;保留以下当前记录的并发修改后可直接重试,不存在时重新定位。\n'
|
|
240
|
+
+ JSON.stringify({ current: currentRecord }),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
if (action === 'review') {
|
|
244
|
+
signal?.throwIfAborted();
|
|
245
|
+
try {
|
|
246
|
+
const reviewed = await memoryService.updateWithVersion(input.id, input.scope, expectedVersion, { status: 'needs_review', confirmationContext: `待复核原因(非确认事实):${input.reason}` }, input.scope === 'workspace' ? ctx.workspaceId : undefined);
|
|
247
|
+
return reviewed ? { ok: true, content: `已标记待复核,原事实和证据未改写。id=${input.id}\nversion=${reviewed.version}` }
|
|
248
|
+
: { ok: false, error: '记录已不存在,未做修改' };
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
if (error.message === 'MEMORY_VERSION_CONFLICT')
|
|
252
|
+
return { ok: false, error: '记忆并发变更,请重新读取;未标记待复核' };
|
|
253
|
+
throw error;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const statement = input.statement;
|
|
257
|
+
const confirmed = action === 'confirm' && await confirmMemoryCorrection(currentRecord.record.subject, currentRecord.record.statement, statement, input.reason, signal);
|
|
258
|
+
if (action === 'confirm' && !confirmed)
|
|
259
|
+
return { ok: false, error: '未获得用户确认(拒绝或宿主无法交互),原记忆未改写;可标记待复核,不要重复请求确认' };
|
|
260
|
+
const evidenceQuote = confirmed ? `用户通过纠正记忆确认:${statement}` : input.evidenceQuote;
|
|
261
|
+
const sensitivity = currentRecord.record.sensitivity === 'sensitive' ? 'sensitive' : input.sensitivity ?? 'normal';
|
|
219
262
|
if (sensitivity === 'sensitive' &&
|
|
220
|
-
|
|
263
|
+
!confirmed && statement.trim() === evidenceQuote.trim()) {
|
|
221
264
|
return {
|
|
222
265
|
ok: false,
|
|
223
266
|
error: '敏感内容不得逐字更新;请先概括、去除不必要细节后再请求确认',
|
|
224
267
|
};
|
|
225
268
|
}
|
|
226
|
-
const evidence = await confirmSensitiveMemory(
|
|
269
|
+
const evidence = confirmed ? 'user_confirmed' : await confirmSensitiveMemory(statement, sensitivity, signal);
|
|
227
270
|
if (!evidence)
|
|
228
271
|
return { ok: false, error: '敏感记忆更新未获确认' };
|
|
229
|
-
const expectedVersion = input.expectedVersion ?? currentRecord.version;
|
|
230
272
|
let updated;
|
|
231
273
|
try {
|
|
232
274
|
updated = await memoryService.updateWithVersion(input.id, input.scope, expectedVersion, {
|
|
@@ -234,9 +276,11 @@ export const updateMemoryTool = {
|
|
|
234
276
|
? { category: input.category }
|
|
235
277
|
: {}),
|
|
236
278
|
...(input.subject ? { subject: input.subject } : {}),
|
|
237
|
-
statement
|
|
279
|
+
statement,
|
|
280
|
+
status: 'active',
|
|
238
281
|
evidence,
|
|
239
|
-
evidenceQuote
|
|
282
|
+
evidenceQuote,
|
|
283
|
+
confirmationContext: confirmed ? `宿主确认纠正:${input.reason}` : '',
|
|
240
284
|
sourceSessionId: ctx.sessionId ?? 'unknown',
|
|
241
285
|
sourceMessageId: ctx.userMessageId ?? 'unknown',
|
|
242
286
|
sensitivity,
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
EXPLICIT_MEMORY_INTENT,
|
|
19
19
|
EXPLICIT_PERSONA_INTENT,
|
|
20
20
|
confirmSensitiveMemory,
|
|
21
|
+
confirmMemoryCorrection,
|
|
21
22
|
denyUnlessLocalOwner,
|
|
22
23
|
requireCurrentEvidence,
|
|
23
24
|
} from './policy.js';
|
|
@@ -39,7 +40,7 @@ function formatRecord(
|
|
|
39
40
|
? ` workspace=${record.workspaceId}`
|
|
40
41
|
: '';
|
|
41
42
|
const lifecycle = ` status=${record.status ?? 'active'} revision=${record.revision ?? 1}`;
|
|
42
|
-
return `- [${record.id}] scope=${record.scope} ${record.category}/${record.subject}${workspace}${lifecycle}${version ? ` version=${version}` : ''}: ${record.statement}(${record.evidence},${new Date(record.updatedAt).toISOString()}
|
|
43
|
+
return `- [${record.id}] scope=${record.scope} ${record.category}/${record.subject}${workspace}${lifecycle}${version ? ` version=${version}` : ''}: ${record.statement}(${record.evidence},${new Date(record.updatedAt).toISOString()})${record.status === 'needs_review' && record.confirmationContext ? `\n复核线索(非事实):${record.confirmationContext}` : ''}`;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
function diagnosticSuffix(
|
|
@@ -188,15 +189,18 @@ export const rememberMemoryTool: Tool<z.infer<typeof rememberSchema>> = {
|
|
|
188
189
|
};
|
|
189
190
|
|
|
190
191
|
const updateSchema = z.object({
|
|
192
|
+
action: z.enum(['update', 'review', 'confirm']).optional().describe('默认 update:当前用户原话纠正;review:仅标记待复核,不改事实;confirm:通过宿主向用户确认拟更新内容后保存'),
|
|
193
|
+
reason: z.string().min(1).max(1000).optional().describe('review/confirm 必填:冲突或纠正依据,注明来源;不得伪造用户陈述'),
|
|
191
194
|
id: z.string().uuid(),
|
|
192
195
|
scope: scopeSchema,
|
|
193
196
|
category: categorySchema.optional(),
|
|
194
197
|
subject: z.string().min(1).max(120).optional(),
|
|
195
|
-
statement: z.string().min(1).max(1000),
|
|
198
|
+
statement: z.string().min(1).max(1000).optional(),
|
|
196
199
|
evidenceQuote: z
|
|
197
200
|
.string()
|
|
198
201
|
.min(1)
|
|
199
202
|
.max(1000)
|
|
203
|
+
.optional()
|
|
200
204
|
.describe('当前用户消息中的逐字证据片段'),
|
|
201
205
|
sensitivity: z.enum(['normal', 'sensitive']).optional(),
|
|
202
206
|
aliases: z.array(z.string().min(1).max(120)).max(20).optional(),
|
|
@@ -217,7 +221,7 @@ const updateSchema = z.object({
|
|
|
217
221
|
export const updateMemoryTool: Tool<z.infer<typeof updateSchema>> = {
|
|
218
222
|
name: 'UpdateMemory',
|
|
219
223
|
description:
|
|
220
|
-
'
|
|
224
|
+
'纠正用户事实记忆:先 RecallMemory 读取 ID/version。默认 update 需要 statement 与当前用户原话 evidenceQuote;review 需要 reason,仅标记待复核、保留原事实与证据;confirm 需要 statement/reason,通过宿主展示新旧内容并取得用户确认再更新。拒绝或无法交互不写入。项目知识和经验走 MemoryMaintenance,不伪造用户证据。',
|
|
221
225
|
inputSchema: updateSchema,
|
|
222
226
|
parameters: toToolParameters(updateSchema),
|
|
223
227
|
isReadOnly: false,
|
|
@@ -228,8 +232,20 @@ export const updateMemoryTool: Tool<z.infer<typeof updateSchema>> = {
|
|
|
228
232
|
const denied = denyUnlessLocalOwner(ctx);
|
|
229
233
|
if (denied) return denied;
|
|
230
234
|
const current = ctx.currentUserInput ?? '';
|
|
231
|
-
const
|
|
232
|
-
if (
|
|
235
|
+
const action = input.action ?? 'update';
|
|
236
|
+
if (action === 'review' && (input.statement !== undefined || input.evidenceQuote !== undefined)) {
|
|
237
|
+
return { ok: false, error: 'review 仅提交 reason,不提交 statement/evidenceQuote;标记疑点不代表确认新事实' };
|
|
238
|
+
}
|
|
239
|
+
if (action !== 'update' && (input.category !== undefined || input.subject !== undefined || input.aliases !== undefined || input.supersedesIds !== undefined || input.sensitivity !== undefined)) {
|
|
240
|
+
return { ok: false, error: 'review/confirm 只处理当前记录的复核状态或正文,不同时修改分类、敏感级别、别名或替代其他记录' };
|
|
241
|
+
}
|
|
242
|
+
if (action === 'update') {
|
|
243
|
+
if (!input.statement || !input.evidenceQuote) return { ok: false, error: 'update 需要 statement 和当前用户原话 evidenceQuote' };
|
|
244
|
+
const invalidEvidence = requireCurrentEvidence(ctx, input.evidenceQuote);
|
|
245
|
+
if (invalidEvidence) return invalidEvidence;
|
|
246
|
+
} else if (!input.reason || (action === 'confirm' && !input.statement)) {
|
|
247
|
+
return { ok: false, error: 'review 需要 reason;confirm 需要 reason 和 statement' };
|
|
248
|
+
}
|
|
233
249
|
if (input.scope === 'workspace' && !ctx.workspaceId) {
|
|
234
250
|
return {
|
|
235
251
|
ok: false,
|
|
@@ -238,7 +254,7 @@ export const updateMemoryTool: Tool<z.infer<typeof updateSchema>> = {
|
|
|
238
254
|
}
|
|
239
255
|
if (
|
|
240
256
|
!(await memoryService.settings()).autoRemember &&
|
|
241
|
-
!EXPLICIT_MEMORY_INTENT.test(current)
|
|
257
|
+
!EXPLICIT_MEMORY_INTENT.test(current) && action !== 'confirm'
|
|
242
258
|
) {
|
|
243
259
|
return {
|
|
244
260
|
ok: false,
|
|
@@ -251,23 +267,49 @@ export const updateMemoryTool: Tool<z.infer<typeof updateSchema>> = {
|
|
|
251
267
|
input.scope === 'workspace' ? ctx.workspaceId : undefined,
|
|
252
268
|
);
|
|
253
269
|
if (!currentRecord) return { ok: false, error: '未找到该记忆,未做修改' };
|
|
254
|
-
const
|
|
270
|
+
const expectedVersion = input.expectedVersion ?? currentRecord.version;
|
|
271
|
+
if (expectedVersion !== currentRecord.version) {
|
|
272
|
+
return {
|
|
273
|
+
ok: false,
|
|
274
|
+
error:
|
|
275
|
+
'记忆已被手工或并发修改。未应用本次更新;保留以下当前记录的并发修改后可直接重试,不存在时重新定位。\n'
|
|
276
|
+
+ JSON.stringify({ current: currentRecord }),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
if (action === 'review') {
|
|
280
|
+
signal?.throwIfAborted();
|
|
281
|
+
try {
|
|
282
|
+
const reviewed = await memoryService.updateWithVersion(input.id, input.scope, expectedVersion,
|
|
283
|
+
{ status: 'needs_review', confirmationContext: `待复核原因(非确认事实):${input.reason}` },
|
|
284
|
+
input.scope === 'workspace' ? ctx.workspaceId : undefined);
|
|
285
|
+
return reviewed ? { ok: true, content: `已标记待复核,原事实和证据未改写。id=${input.id}\nversion=${reviewed.version}` }
|
|
286
|
+
: { ok: false, error: '记录已不存在,未做修改' };
|
|
287
|
+
} catch (error) {
|
|
288
|
+
if ((error as Error).message === 'MEMORY_VERSION_CONFLICT') return { ok: false, error: '记忆并发变更,请重新读取;未标记待复核' };
|
|
289
|
+
throw error;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const statement = input.statement!;
|
|
293
|
+
const confirmed = action === 'confirm' && await confirmMemoryCorrection(currentRecord.record.subject,
|
|
294
|
+
currentRecord.record.statement, statement, input.reason!, signal);
|
|
295
|
+
if (action === 'confirm' && !confirmed) return { ok: false, error: '未获得用户确认(拒绝或宿主无法交互),原记忆未改写;可标记待复核,不要重复请求确认' };
|
|
296
|
+
const evidenceQuote = confirmed ? `用户通过纠正记忆确认:${statement}` : input.evidenceQuote!;
|
|
297
|
+
const sensitivity = currentRecord.record.sensitivity === 'sensitive' ? 'sensitive' : input.sensitivity ?? 'normal';
|
|
255
298
|
if (
|
|
256
299
|
sensitivity === 'sensitive' &&
|
|
257
|
-
|
|
300
|
+
!confirmed && statement.trim() === evidenceQuote.trim()
|
|
258
301
|
) {
|
|
259
302
|
return {
|
|
260
303
|
ok: false,
|
|
261
304
|
error: '敏感内容不得逐字更新;请先概括、去除不必要细节后再请求确认',
|
|
262
305
|
};
|
|
263
306
|
}
|
|
264
|
-
const evidence = await confirmSensitiveMemory(
|
|
265
|
-
|
|
307
|
+
const evidence = confirmed ? 'user_confirmed' : await confirmSensitiveMemory(
|
|
308
|
+
statement,
|
|
266
309
|
sensitivity,
|
|
267
310
|
signal,
|
|
268
311
|
);
|
|
269
312
|
if (!evidence) return { ok: false, error: '敏感记忆更新未获确认' };
|
|
270
|
-
const expectedVersion = input.expectedVersion ?? currentRecord.version;
|
|
271
313
|
let updated: Awaited<ReturnType<typeof memoryService.updateWithVersion>>;
|
|
272
314
|
try {
|
|
273
315
|
updated = await memoryService.updateWithVersion(
|
|
@@ -279,9 +321,11 @@ export const updateMemoryTool: Tool<z.infer<typeof updateSchema>> = {
|
|
|
279
321
|
? { category: input.category as MemoryCategory }
|
|
280
322
|
: {}),
|
|
281
323
|
...(input.subject ? { subject: input.subject } : {}),
|
|
282
|
-
statement
|
|
324
|
+
statement,
|
|
325
|
+
status: 'active',
|
|
283
326
|
evidence,
|
|
284
|
-
evidenceQuote
|
|
327
|
+
evidenceQuote,
|
|
328
|
+
confirmationContext: confirmed ? `宿主确认纠正:${input.reason}` : '',
|
|
285
329
|
sourceSessionId: ctx.sessionId ?? 'unknown',
|
|
286
330
|
sourceMessageId: ctx.userMessageId ?? 'unknown',
|
|
287
331
|
sensitivity,
|
|
@@ -25,9 +25,24 @@ export function requireCurrentEvidence(ctx, evidenceQuote) {
|
|
|
25
25
|
? null
|
|
26
26
|
: {
|
|
27
27
|
ok: false,
|
|
28
|
-
error: 'evidenceQuote
|
|
28
|
+
error: 'evidenceQuote 不是当前用户消息的逐字片段,未写入。不要改写或伪造引用重试:用户事实可用 UpdateMemory(action=confirm) 请求确认,或 action=review 标记待复核;项目知识/经验使用 MemoryMaintenance,不把工具证据当用户原话。',
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
|
+
export async function confirmMemoryCorrection(subject, previous, proposed, reason, signal) {
|
|
32
|
+
if (!hasAskHandler())
|
|
33
|
+
return false;
|
|
34
|
+
signal?.throwIfAborted();
|
|
35
|
+
const [answer] = await requestUserChoice([{
|
|
36
|
+
header: '纠正记忆',
|
|
37
|
+
question: `是否更新“${subject}”?\n原记录:${previous}\n拟更新为:${proposed}\n纠正依据:${reason}`,
|
|
38
|
+
options: [
|
|
39
|
+
{ label: '暂不更新', description: '保留原记录,不把建议视为事实。' },
|
|
40
|
+
{ label: '确认更新', description: '确认拟更新内容准确,并保存为本机长期记忆。' },
|
|
41
|
+
],
|
|
42
|
+
}], signal);
|
|
43
|
+
signal?.throwIfAborted();
|
|
44
|
+
return answer?.selected.length === 1 && answer.selected[0] === '确认更新';
|
|
45
|
+
}
|
|
31
46
|
export async function confirmSensitiveMemory(statement, sensitivity, signal) {
|
|
32
47
|
if (sensitivity === 'normal')
|
|
33
48
|
return 'user_stated';
|