@nowcrew/daemon 0.6.88 → 0.6.90
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 +66 -4
- package/dist/agent-ability/controller.js +11 -1
- package/dist/agent-ability/git-errors.js +22 -0
- package/dist/agent-ability/materializer.js +21 -10
- package/dist/agent-ability/resolver.js +12 -4
- package/dist/agent-environment-contract.js +55 -0
- package/dist/agent-environment-control.js +24 -0
- package/dist/agent-environment-execution.js +98 -0
- package/dist/agent-environments.js +275 -0
- package/dist/automatic-daemon-installer.js +51 -2
- package/dist/automatic-install-target.js +8 -0
- package/dist/computer-main.js +18 -0
- package/dist/computer-profile.js +17 -4
- package/dist/computer-setup-client.js +318 -0
- package/dist/computer-setup-orchestration.js +203 -0
- package/dist/computer-setup.js +244 -0
- package/dist/config.js +30 -12
- package/dist/console-formatter.js +3 -3
- package/dist/console.js +15 -2
- package/dist/daemon-update-eligibility.js +1 -1
- package/dist/execution-journal.js +9 -6
- package/dist/execution-protocol.js +1 -0
- package/dist/execution-runner.js +13 -8
- package/dist/i18n.js +19 -0
- package/dist/list-models.js +12 -12
- package/dist/local-execution-lease.js +22 -0
- package/dist/local-executor.js +57 -84
- package/dist/local-memory/candidate.js +43 -0
- package/dist/local-memory/index-budget.js +8 -0
- package/dist/local-memory/inventory-cli.js +61 -0
- package/dist/local-memory/inventory-preview.js +159 -0
- package/dist/local-memory/inventory-source.js +159 -0
- package/dist/local-memory/pending-match.js +29 -0
- package/dist/local-memory/pending-query.js +100 -0
- package/dist/local-memory/pending-schema.js +29 -0
- package/dist/local-memory/pending-source.js +126 -0
- package/dist/local-memory/pending.js +34 -84
- package/dist/local-memory/prompt.js +19 -12
- package/dist/local-memory/read-cli.js +97 -12
- package/dist/local-memory/rollout.js +82 -0
- package/dist/local-memory/runtime.js +23 -17
- package/dist/local-memory/work-log-execution.js +31 -0
- package/dist/local-memory/work-log.js +58 -0
- package/dist/local-memory/write.js +27 -58
- package/dist/local-runtime-environment.js +50 -0
- package/dist/machine-environment-cache.js +147 -0
- package/dist/machine-environment-contract.js +10 -0
- package/dist/machine-environment-control.js +27 -0
- package/dist/machine-environment-policy.js +75 -0
- package/dist/machine-environment-process.js +110 -0
- package/dist/machine-info.js +70 -25
- package/dist/machine-runtime-environment.js +189 -0
- package/dist/machine-shell-environment-capture.js +135 -0
- package/dist/machine-windows-environment-capture.js +215 -0
- package/dist/machine-windows-environment-script.js +158 -0
- package/dist/main.js +4 -69
- package/dist/marketplace-command.js +21 -0
- package/dist/marketplace-controller.js +80 -0
- package/dist/marketplace-protocol.js +38 -0
- package/dist/marketplace-remote.js +48 -0
- package/dist/marketplace-runner.js +235 -0
- package/dist/marketplace-serve.js +17 -0
- package/dist/marketplace-source.js +103 -0
- package/dist/message-file-preview.js +246 -0
- package/dist/pre-admission-rejection.js +64 -0
- package/dist/project-skills/execution-adapter.js +3 -2
- package/dist/project-skills/runtime-launch.js +14 -0
- package/dist/provider-env.js +26 -15
- package/dist/runner.js +17 -4
- package/dist/runtime-environment-merge.js +16 -0
- package/dist/runtime-health-handler.js +10 -0
- package/dist/runtime-health.js +3 -1
- package/dist/runtime-path.js +22 -12
- package/dist/runtime-probe.js +35 -6
- package/dist/runtimes/acp-probe-transport.js +69 -0
- package/dist/runtimes/claude-models.js +20 -9
- package/dist/runtimes/deepseek-harness-launch.js +10 -0
- package/dist/runtimes/deepseek-harness-profile.js +92 -0
- package/dist/runtimes/deepseek-harness-runner.js +226 -0
- package/dist/runtimes/deepseek-harness.js +76 -15
- package/dist/runtimes/harness-probe-retry.js +42 -0
- package/dist/runtimes/hermes-models.js +2 -1
- package/dist/runtimes/kimi-acp-runner.js +78 -13
- package/dist/runtimes/opencode.js +2 -2
- package/dist/serve.js +88 -176
- package/dist/server-url.js +25 -0
- package/dist/slog.js +2 -1
- package/dist/storage-identity.js +0 -3
- package/dist/storage-runtime.js +2 -2
- package/dist/storage-serve-adapter.js +6 -72
- package/dist/supervised-runtime.js +17 -2
- package/dist/token.js +0 -1
- package/dist/workspace-control-handler.js +27 -0
- package/dist/workspace-fs.js +66 -15
- package/dist/workspace.js +1 -1
- package/package.json +6 -4
- package/dist/codex-home-launch.js +0 -7
- package/dist/codex-home-mode-controller.js +0 -68
- package/dist/daemon-migration-controller.js +0 -211
- package/dist/daemon-migration-wiring.js +0 -22
- package/dist/runtimes/codex-home-migration-cli.js +0 -26
- package/dist/runtimes/codex-home-migration.js +0 -112
- package/dist/runtimes/codex-home.js +0 -236
- package/dist/storage-control.js +0 -212
- package/dist/storage-copy.js +0 -628
- package/dist/storage-credential-guard.js +0 -21
- package/dist/storage-doctor.js +0 -228
- package/dist/storage-manifest.js +0 -1027
- package/dist/storage-migration-controller.js +0 -440
- package/dist/storage-migration-manifest.js +0 -105
- package/dist/storage-migration-policy.js +0 -100
- package/dist/storage-runtime-controller.js +0 -381
- package/dist/storage-runtime-readiness.js +0 -240
- package/dist/storage-safe-fs.js +0 -302
- package/dist/storage-status-delivery.js +0 -224
- package/dist/storage-status-replay.js +0 -140
- package/dist/storage-target-confirmation.js +0 -129
- package/dist/storage-worker-command.js +0 -37
- package/dist/storage-worker-executor.js +0 -449
- package/dist/storage-worker-handoff.js +0 -151
- package/dist/storage-worker-operation.js +0 -294
- package/dist/storage-worker-status.js +0 -259
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { dslog } from "../slog.js";
|
|
2
2
|
import { renderPendingMemoryContext, searchPendingMemoryCandidates } from "./pending.js";
|
|
3
|
-
import { renderLocalMemoryResultInstructions } from "./runtime.js";
|
|
3
|
+
import { renderLocalMemoryResultInstructions, renderLocalMemoryV2WriteGuard } from "./runtime.js";
|
|
4
|
+
import { LOCAL_MEMORY_V2_DEFERRED_GUIDANCE } from "./rollout.js";
|
|
5
|
+
const PENDING_READ_GUIDANCE = "\n\nV2新增资料预览未命中不代表没有资料;先查有效索引,必要时用 crew-memory search-pending --query '关键词',按 nextCursor 续查;用 read-pending --id ID 分页读正文。容量暂存资料不能覆盖现行规则,未应用修改不作为记忆。用 crew-memory --help 查看参数。\n";
|
|
4
6
|
export async function prepareLocalMemoryPrompt(input) {
|
|
5
7
|
let pending = "";
|
|
6
8
|
if (input.enabled) {
|
|
@@ -20,19 +22,24 @@ export async function prepareLocalMemoryPrompt(input) {
|
|
|
20
22
|
}
|
|
21
23
|
}
|
|
22
24
|
const original = input.renderBase(0);
|
|
25
|
+
const guard = input.deferred ? LOCAL_MEMORY_V2_DEFERRED_GUIDANCE
|
|
26
|
+
: input.strictRollout && input.enabled && input.resultPath === null ? renderLocalMemoryV2WriteGuard() : "";
|
|
23
27
|
const structured = input.resultPath === null ? "" : renderLocalMemoryResultInstructions(input.resultPath);
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
if (
|
|
34
|
-
return { systemPrompt
|
|
28
|
+
const guidance = input.enabled ? PENDING_READ_GUIDANCE : "";
|
|
29
|
+
// 短命令指引放在当前轮,独立于可舍弃预览;收尾契约仍保持系统与当前轮两份。
|
|
30
|
+
for (const choice of [
|
|
31
|
+
{ suffix: pending + guard + structured, wake: guard + structured + guidance },
|
|
32
|
+
{ suffix: guard + structured, wake: guard + structured + guidance },
|
|
33
|
+
{ suffix: guard + structured, wake: guard + structured },
|
|
34
|
+
]) {
|
|
35
|
+
const reserved = Buffer.byteLength(choice.suffix + choice.wake, "utf8");
|
|
36
|
+
const systemPrompt = (reserved === 0 ? original : input.renderBase(reserved)) + choice.suffix;
|
|
37
|
+
if (input.maxBytes === undefined || Buffer.byteLength(systemPrompt + choice.wake, "utf8") <= input.maxBytes) {
|
|
38
|
+
return { systemPrompt, resultPath: input.resultPath, wakePromptSuffix: choice.wake };
|
|
39
|
+
}
|
|
35
40
|
}
|
|
41
|
+
if (input.strictRollout)
|
|
42
|
+
throw new Error("local_memory_v2_prompt_over_budget");
|
|
36
43
|
// 强制基础指令与契约本身仍无法共存时,保留既有非结构化兼容回退,不裁基础指令。
|
|
37
44
|
return { systemPrompt: original, resultPath: null, wakePromptSuffix: "" };
|
|
38
45
|
}
|
|
@@ -5,8 +5,13 @@ import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
6
|
import { parseArgs, TextDecoder } from "node:util";
|
|
7
7
|
import { z } from "zod";
|
|
8
|
+
import { readPendingMemoryBody, searchPendingMemoryPage } from "./pending-query.js";
|
|
8
9
|
const OUTPUT_BYTES = 12 * 1024;
|
|
9
10
|
const FILE_BYTES = 32 * 1024 * 1024;
|
|
11
|
+
const READ_LIMIT_MAX = 4_000;
|
|
12
|
+
const READ_LIMIT_SAFE = 2_000;
|
|
13
|
+
const SEARCH_LIMIT_MAX = 40;
|
|
14
|
+
const SEARCH_LIMIT_SAFE = 10;
|
|
10
15
|
const FileSchema = z.string().min(1).refine((file) => {
|
|
11
16
|
if (isAbsolute(file) || /[\\\u0000-\u001f\u007f]/u.test(file))
|
|
12
17
|
return false;
|
|
@@ -25,22 +30,43 @@ const CommandSchema = z.discriminatedUnion("command", [
|
|
|
25
30
|
z.object({
|
|
26
31
|
command: z.literal("search"), ...Common,
|
|
27
32
|
query: z.string().trim().min(1).max(256),
|
|
28
|
-
limit: z.coerce.number().int().min(1).max(
|
|
33
|
+
limit: z.coerce.number().int().min(1).max(Number.MAX_SAFE_INTEGER).default(10),
|
|
29
34
|
afterLine: z.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).default(0),
|
|
30
35
|
}).strict(),
|
|
31
36
|
z.object({
|
|
32
37
|
command: z.literal("read"), ...Common,
|
|
33
38
|
offset: z.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).default(0),
|
|
34
|
-
limit: z.coerce.number().int().min(1).max(
|
|
39
|
+
limit: z.coerce.number().int().min(1).max(Number.MAX_SAFE_INTEGER).default(2000),
|
|
40
|
+
}).strict(),
|
|
41
|
+
z.object({
|
|
42
|
+
command: z.literal("search-pending"),
|
|
43
|
+
query: z.string().trim().min(1).max(256),
|
|
44
|
+
cursor: z.string().min(1).max(4096).optional(),
|
|
45
|
+
limit: z.coerce.number().int().min(1).max(Number.MAX_SAFE_INTEGER).default(10),
|
|
46
|
+
}).strict(),
|
|
47
|
+
z.object({
|
|
48
|
+
command: z.literal("read-pending"),
|
|
49
|
+
id: z.string().min(1).max(250).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u),
|
|
50
|
+
ifHash: Common.ifHash,
|
|
51
|
+
offset: z.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).default(0),
|
|
52
|
+
limit: z.coerce.number().int().min(1).max(Number.MAX_SAFE_INTEGER).default(2000),
|
|
35
53
|
}).strict(),
|
|
36
54
|
]);
|
|
37
55
|
const USAGE = `只读记忆工具,默认输出 JSON,不修改任何记忆文件。
|
|
38
56
|
crew-memory search --query <关键词> [--file MEMORY.md|notes/文件.md] [--limit 10] [--after-line 0]
|
|
39
57
|
crew-memory read [--file MEMORY.md|notes/文件.md] [--offset 0] [--limit 2000]
|
|
40
|
-
|
|
58
|
+
crew-memory search-pending --query <关键词> [--cursor <游标>] [--limit 10]
|
|
59
|
+
crew-memory read-pending --id <候选ID> [--offset 0] [--limit 2000] [--if-hash <sha256>]
|
|
60
|
+
read/read-pending 的有效页长为 1—4000,默认 2000;search/search-pending 的有效页长为 1—40,默认 10。超出上限时工具自动使用安全页长并返回 warning(recovered=true),不把它当作读取失败。
|
|
61
|
+
search 关键词按空白分隔,同一行须全部命中;返回完整文件中的行号和字符偏移。
|
|
41
62
|
读取的 offset/limit 按 Unicode 字符计数;用 nextOffset 继续,eof=true 表示结束。
|
|
63
|
+
读取的 --limit 超出范围时,工具会自动降到 2000 并在结果中标记 warning;保留原文件或候选 ID 及 offset,按 nextOffset 分页,不要一次读完整文件。
|
|
42
64
|
后续页可传 --if-hash <前一页的 sha256>,文件变化时停止,避免混读不同版本。
|
|
43
|
-
|
|
65
|
+
search/read 读取 MEMORY.md 和 notes 原文,不过滤失效段落,也不扫描 pending。
|
|
66
|
+
search-pending 只查因容量延期的 create 候选;有 nextCursor 就继续,即使本页 matches 为空。
|
|
67
|
+
complete=true 才表示全部候选已检索完成且无遗漏;warnings 或非零退出码表示读取未完成,不能当作零命中。
|
|
68
|
+
read-pending 只返回候选正文;offset 非零时必须传 --if-hash,update/supersede 候选不可读取。
|
|
69
|
+
候选只供相关问题参考,未生效,不能覆盖有效规则;先读取当前有效索引与明细。`;
|
|
44
70
|
function inside(root, path) {
|
|
45
71
|
const value = relative(root, path);
|
|
46
72
|
return value !== ".." && !value.startsWith(`..${sep}`) && !isAbsolute(value);
|
|
@@ -109,7 +135,15 @@ function originalLowercaseIndex(text, index) {
|
|
|
109
135
|
}
|
|
110
136
|
return text.length;
|
|
111
137
|
}
|
|
112
|
-
function
|
|
138
|
+
function normalizeLimit(requestedLimit, max, safe) {
|
|
139
|
+
if (requestedLimit <= max)
|
|
140
|
+
return { limit: requestedLimit };
|
|
141
|
+
return {
|
|
142
|
+
limit: safe,
|
|
143
|
+
warning: { code: "memory_limit_clamped", requestedLimit, appliedLimit: safe, recovered: true },
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function searchSource(text, sha256, command, warning) {
|
|
113
147
|
const terms = command.query.toLowerCase().split(/\s+/u);
|
|
114
148
|
const matches = [];
|
|
115
149
|
let line = 0;
|
|
@@ -150,10 +184,22 @@ function searchSource(text, sha256, command) {
|
|
|
150
184
|
return {
|
|
151
185
|
file: command.file, sha256, matches, matchCount, hasMore,
|
|
152
186
|
nextAfterLine: hasMore ? matches.at(-1)?.line ?? null : null,
|
|
187
|
+
...(warning === undefined ? {} : { warning, requestedLimit: warning.requestedLimit, limit: warning.appliedLimit }),
|
|
153
188
|
};
|
|
154
189
|
}
|
|
155
|
-
function readPage(text,
|
|
156
|
-
const metadata = {
|
|
190
|
+
function readPage(text, source, command, warning) {
|
|
191
|
+
const metadata = {
|
|
192
|
+
...source,
|
|
193
|
+
...(warning === undefined ? {} : {
|
|
194
|
+
warning,
|
|
195
|
+
requestedLimit: warning.requestedLimit,
|
|
196
|
+
limit: warning.appliedLimit,
|
|
197
|
+
}),
|
|
198
|
+
offset: command.offset,
|
|
199
|
+
nextOffset: Number.MAX_SAFE_INTEGER,
|
|
200
|
+
eof: false,
|
|
201
|
+
content: "",
|
|
202
|
+
};
|
|
157
203
|
let remaining = OUTPUT_BYTES - Buffer.byteLength(JSON.stringify(metadata), "utf8");
|
|
158
204
|
let position = 0;
|
|
159
205
|
let offset = 0;
|
|
@@ -175,8 +221,16 @@ function readPage(text, sha256, command) {
|
|
|
175
221
|
count += 1;
|
|
176
222
|
}
|
|
177
223
|
const eof = position === text.length;
|
|
224
|
+
if (!eof && count === 0)
|
|
225
|
+
throw new Error("memory_output_too_large");
|
|
178
226
|
return { ...metadata, content, eof, nextOffset: eof ? null : offset };
|
|
179
227
|
}
|
|
228
|
+
function writeResult(result, io) {
|
|
229
|
+
const output = JSON.stringify(result);
|
|
230
|
+
if (Buffer.byteLength(output, "utf8") > OUTPUT_BYTES)
|
|
231
|
+
throw new Error("memory_output_too_large");
|
|
232
|
+
io.out(output);
|
|
233
|
+
}
|
|
180
234
|
export async function runMemoryReadCommand(argv, io) {
|
|
181
235
|
let command;
|
|
182
236
|
try {
|
|
@@ -186,6 +240,7 @@ export async function runMemoryReadCommand(argv, io) {
|
|
|
186
240
|
query: { type: "string", short: "q" }, file: { type: "string" },
|
|
187
241
|
limit: { type: "string" }, offset: { type: "string" },
|
|
188
242
|
"after-line": { type: "string" }, "if-hash": { type: "string" },
|
|
243
|
+
id: { type: "string" }, cursor: { type: "string" },
|
|
189
244
|
json: { type: "boolean" }, help: { type: "boolean", short: "h" },
|
|
190
245
|
},
|
|
191
246
|
});
|
|
@@ -201,24 +256,54 @@ export async function runMemoryReadCommand(argv, io) {
|
|
|
201
256
|
...(afterLine === undefined ? {} : { afterLine }),
|
|
202
257
|
...(ifHash === undefined ? {} : { ifHash }),
|
|
203
258
|
});
|
|
259
|
+
if (command.command === "read-pending" && command.offset > 0 && command.ifHash === undefined) {
|
|
260
|
+
throw new Error("memory_hash_required");
|
|
261
|
+
}
|
|
204
262
|
}
|
|
205
263
|
catch {
|
|
206
|
-
io.err(`memory_arguments_invalid:
|
|
264
|
+
io.err(`memory_arguments_invalid: 参数不合法,记忆读取未完成;请按以下参数范围修正后重试,不要当作没有记忆。\n${USAGE}`);
|
|
207
265
|
return 2;
|
|
208
266
|
}
|
|
209
267
|
try {
|
|
268
|
+
if (!io.env.CREW_HOME)
|
|
269
|
+
throw new Error("memory_home_missing");
|
|
270
|
+
if (command.command === "search-pending") {
|
|
271
|
+
const normalized = normalizeLimit(command.limit, SEARCH_LIMIT_MAX, SEARCH_LIMIT_SAFE);
|
|
272
|
+
const result = await searchPendingMemoryPage({ agentDir: io.env.CREW_HOME, query: command.query,
|
|
273
|
+
...(command.cursor === undefined ? {} : { cursor: command.cursor }), limit: normalized.limit });
|
|
274
|
+
writeResult({ ...result, ...(normalized.warning === undefined ? {} : {
|
|
275
|
+
warning: normalized.warning, requestedLimit: normalized.warning.requestedLimit, limit: normalized.warning.appliedLimit,
|
|
276
|
+
}) }, io);
|
|
277
|
+
if (result.warnings.length > 0 || (!result.hasMore && !result.complete)) {
|
|
278
|
+
throw new Error("memory_pending_search_incomplete");
|
|
279
|
+
}
|
|
280
|
+
return 0;
|
|
281
|
+
}
|
|
282
|
+
if (command.command === "read-pending") {
|
|
283
|
+
const normalized = normalizeLimit(command.limit, READ_LIMIT_MAX, READ_LIMIT_SAFE);
|
|
284
|
+
const { content, ...metadata } = await readPendingMemoryBody({ agentDir: io.env.CREW_HOME,
|
|
285
|
+
pendingId: command.id, ...(command.ifHash === undefined ? {} : { ifHash: command.ifHash }) });
|
|
286
|
+
writeResult(readPage(content, metadata, { ...command, limit: normalized.limit }, normalized.warning), io);
|
|
287
|
+
return 0;
|
|
288
|
+
}
|
|
210
289
|
const source = await readSource(io.env.CREW_HOME, command.file);
|
|
211
290
|
if (command.ifHash !== undefined && command.ifHash !== source.sha256)
|
|
212
291
|
throw new Error("memory_file_changed");
|
|
213
292
|
const result = command.command === "search"
|
|
214
|
-
?
|
|
215
|
-
|
|
216
|
-
|
|
293
|
+
? (() => {
|
|
294
|
+
const normalized = normalizeLimit(command.limit, SEARCH_LIMIT_MAX, SEARCH_LIMIT_SAFE);
|
|
295
|
+
return searchSource(source.text, source.sha256, { ...command, limit: normalized.limit }, normalized.warning);
|
|
296
|
+
})()
|
|
297
|
+
: (() => {
|
|
298
|
+
const normalized = normalizeLimit(command.limit, READ_LIMIT_MAX, READ_LIMIT_SAFE);
|
|
299
|
+
return readPage(source.text, { file: command.file, sha256: source.sha256 }, { ...command, limit: normalized.limit }, normalized.warning);
|
|
300
|
+
})();
|
|
301
|
+
writeResult(result, io);
|
|
217
302
|
return 0;
|
|
218
303
|
}
|
|
219
304
|
catch (error) {
|
|
220
305
|
const code = error.code;
|
|
221
|
-
const known = error instanceof Error && /^memory_[a-z_]+$/u.test(error.message) ? error.message : code ?? "memory_read_failed";
|
|
306
|
+
const known = error instanceof Error && /^(?:local_)?memory_[a-z_]+$/u.test(error.message) ? error.message : code ?? "memory_read_failed";
|
|
222
307
|
io.err(JSON.stringify({ error: known, message: "记忆读取未完成;检查路径、文件版本或大小,不要将此错误当作没有记忆。" }));
|
|
223
308
|
return 1;
|
|
224
309
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { lstat, readdir, realpath } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { localMemoryV2Environment } from "../config.js";
|
|
4
|
+
import { memorySeed } from "../workspace.js";
|
|
5
|
+
import { LOCAL_MEMORY_INDEX_CODE_POINT_CAP, LOCAL_MEMORY_INDEX_ENTRY_CAP, LOCAL_MEMORY_TOTAL_CODE_POINT_CAP } from "./index-budget.js";
|
|
6
|
+
import { inventoryLimits, readMemoryInventorySource } from "./inventory-source.js";
|
|
7
|
+
import { previewMemoryInventory } from "./inventory-preview.js";
|
|
8
|
+
import { parseLocalMemoryIndex } from "./write.js";
|
|
9
|
+
import { dslog } from "../slog.js";
|
|
10
|
+
export const LOCAL_MEMORY_V2_DEFERRED_GUIDANCE = `
|
|
11
|
+
|
|
12
|
+
## 本轮本地记忆兼容状态
|
|
13
|
+
开发环境已开放 V2,但该 Agent 的旧记忆尚未通过格式兼容检查,本轮未启用结构化 V2。
|
|
14
|
+
本节覆盖前文和旧会话中直接改写共享记忆的要求:本轮 MEMORY.md 与 notes 只读,不得改写、迁移或清空。
|
|
15
|
+
用户要求新增或更新长期规则时,内容和授权均只记录到本任务的 $CREW_TASK_LOG;保留日志,不要清空,不要宣称长期记忆已生效。旧资料兼容处理完成前,记忆收尾会明确报告需要处理。`;
|
|
16
|
+
export function reportLocalMemoryV2Deferred(executionId, handle, memoryPrune) {
|
|
17
|
+
dslog("local_memory.v2_deferred", "旧记忆尚未通过兼容检查,保留原资料和待处理日志", {
|
|
18
|
+
execution_id: executionId, agent_handle: handle, reason: "requires_migration"
|
|
19
|
+
});
|
|
20
|
+
if (memoryPrune)
|
|
21
|
+
throw new Error("local_memory_v2_requires_migration");
|
|
22
|
+
}
|
|
23
|
+
/** 只核对小型索引和其引用;不扫描全部历史任务、不迁移或生成新存储。 */
|
|
24
|
+
export async function resolveLocalMemoryV2Rollout(input) {
|
|
25
|
+
const environment = localMemoryV2Environment(input.serverUrl);
|
|
26
|
+
if (input.selfTraining || environment === "production")
|
|
27
|
+
return { enabled: false, deferred: false };
|
|
28
|
+
if (environment === "custom")
|
|
29
|
+
return { enabled: input.requested, deferred: false };
|
|
30
|
+
const deferred = { enabled: false, deferred: true };
|
|
31
|
+
const { workspace } = input;
|
|
32
|
+
if (Array.from(workspace.memory).length > LOCAL_MEMORY_TOTAL_CODE_POINT_CAP)
|
|
33
|
+
return deferred;
|
|
34
|
+
try {
|
|
35
|
+
const root = await realpath(workspace.dir);
|
|
36
|
+
const notesStat = await lstat(join(root, "notes"));
|
|
37
|
+
if (!notesStat.isDirectory() || notesStat.isSymbolicLink())
|
|
38
|
+
return deferred;
|
|
39
|
+
const memory = await readMemoryInventorySource(root, "MEMORY.md", inventoryLimits.fileBytes);
|
|
40
|
+
if (memory.content !== workspace.memory)
|
|
41
|
+
return deferred;
|
|
42
|
+
// 完整种子且无旧明细才可按空记忆接入;marker 不能替代这项核对。
|
|
43
|
+
const seededRole = /^## Role\n([\s\S]*?)\n\n## Core Goals\n/mu.exec(memory.content)?.[1];
|
|
44
|
+
if (seededRole !== undefined && memory.content === memorySeed(input.handle, seededRole)) {
|
|
45
|
+
return (await readdir(join(root, "notes"))).length === 0 ? { enabled: true, deferred: false } : deferred;
|
|
46
|
+
}
|
|
47
|
+
const generated = /<!-- NOWWORK:MEMORY_INDEX:BEGIN schema=1 -->[\s\S]*?<!-- NOWWORK:MEMORY_INDEX:END -->/u.exec(memory.content)?.[0];
|
|
48
|
+
if (!generated || Array.from(generated).length > LOCAL_MEMORY_INDEX_CODE_POINT_CAP)
|
|
49
|
+
return deferred;
|
|
50
|
+
const entries = parseLocalMemoryIndex(memory.content);
|
|
51
|
+
if (entries.length > LOCAL_MEMORY_INDEX_ENTRY_CAP)
|
|
52
|
+
return deferred;
|
|
53
|
+
const sources = [memory];
|
|
54
|
+
let remaining = inventoryLimits.totalBytes - memory.bytes;
|
|
55
|
+
for (const file of new Set(entries.map(entry => entry.file))) {
|
|
56
|
+
if (!/^notes\/(preferences|project-rules|testing|debugging|git|incidents|lessons)\.md$/u.test(file))
|
|
57
|
+
return deferred;
|
|
58
|
+
const source = await readMemoryInventorySource(root, file, Math.min(inventoryLimits.fileBytes, remaining));
|
|
59
|
+
sources.push(source);
|
|
60
|
+
remaining -= source.bytes;
|
|
61
|
+
}
|
|
62
|
+
const preview = previewMemoryInventory(sources, true, "existing_v2");
|
|
63
|
+
// 此处只放行现有V2;旧格式和未标记混合卡片都留待显式兼容处理。
|
|
64
|
+
if (preview.entries.some(entry => entry.format !== "managed")
|
|
65
|
+
|| preview.issues.some(issue => !["managed_anchor_invalid", "projection_over_budget"].includes(issue.code)))
|
|
66
|
+
return deferred;
|
|
67
|
+
// 现有writer只改状态时不会重写正文块的revision;兼容门槛不能因此拒绝合法置顶。
|
|
68
|
+
for (const entry of entries) {
|
|
69
|
+
if (!/^[A-Za-z0-9_-]+$/u.test(entry.id))
|
|
70
|
+
return deferred;
|
|
71
|
+
const detail = sources.find(source => source.path === entry.file)?.content ?? "";
|
|
72
|
+
const begins = [...detail.matchAll(new RegExp(`<!-- NOWWORK:MEMORY_ENTRY:BEGIN id=${entry.id} revision=[1-9][0-9]* -->`, "gu"))];
|
|
73
|
+
const end = `<!-- NOWWORK:MEMORY_ENTRY:END id=${entry.id} -->`;
|
|
74
|
+
if (begins.length !== 1 || detail.split(end).length !== 2 || detail.indexOf(end) < begins[0].index)
|
|
75
|
+
return deferred;
|
|
76
|
+
}
|
|
77
|
+
return { enabled: true, deferred: false };
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return deferred;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
|
-
import { lstat, mkdir, open, readFile,
|
|
4
|
-
import {
|
|
3
|
+
import { lstat, mkdir, open, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
5
|
import { applyLocalMemoryResult, parseLocalMemoryResult } from "./write.js";
|
|
6
|
+
import { verifyWorkLogSource } from "./work-log.js";
|
|
6
7
|
export function isLocalMemoryV2Eligible(input) {
|
|
7
8
|
// The allowlist is the rollout boundary. The marker and seed/pre-existing
|
|
8
9
|
// state are retained as diagnostics and for backward-compatible marker
|
|
@@ -69,8 +70,8 @@ outcome 只允许 no_change、proposed、conflict、failed。新增、更新、
|
|
|
69
70
|
\`\`\`
|
|
70
71
|
|
|
71
72
|
每个 changes 项须保留示例中的全部字段。operation=create|update|supersede;kind=procedural|semantic|episodic|preference;priority=high|medium|low;detailCategory=preferences|project-rules|testing|debugging|git|incidents|lessons。create 的 memoryId/baseRevision/supersedesMemoryId 必须为 null;update 按现有索引填写 memoryId/baseRevision,supersedesMemoryId 为 null;supersede 填 supersedesMemoryId/baseRevision,memoryId 为 null。
|
|
72
|
-
只提交可复用、与本次任务明确相关的少量候选;不要写入凭证、token、隐私或内部控制标记。daemon 会校验、去重、限制 MEMORY.md 大小,超出索引容量的候选会保存在 pending
|
|
73
|
-
|
|
73
|
+
只提交可复用、与本次任务明确相关的少量候选;不要写入凭证、token、隐私或内部控制标记。daemon 会校验、去重、限制 MEMORY.md 大小,超出索引容量的候选会保存在 pending 归档中。容量暂存的 create 可按需检索参考但不能覆盖现行规则;有空间的后续 proposed 提交可提升。未应用的 update/supersede 不进入普通召回,必须重新核对目标版本后处理。
|
|
74
|
+
写入候选文件只表示提交候选,不能提前宣称已经记住或规则已经更新;daemon 在本轮结束后才处理,候选持久化与进入有效索引也不是同一结果。本结构化轮次覆盖前文提前清理工作日志的要求:保留 $CREW_TASK_LOG,不要清空或删除;daemon 将在处理成功后清理,失败时保留日志和候选用于排查或重试。
|
|
74
75
|
`;
|
|
75
76
|
}
|
|
76
77
|
/** Prevent ordinary V2 turns from racing the daemon-managed memory writer. */
|
|
@@ -80,6 +81,11 @@ export function renderLocalMemoryV2WriteGuard() {
|
|
|
80
81
|
## V2 shared memory write safety
|
|
81
82
|
本节覆盖前文关于普通任务直接更新共享记忆的例外规则。本 Agent 已启用结构化本地记忆 V2。普通任务不得直接编辑共享的 $CREW_HOME/MEMORY.md 或 $CREW_HOME/notes/ 文件;即使用户明确要求记住内容,也只把候选、用户授权和分类理由记录到本任务的 $CREW_TASK_LOG,留给任务结束后的 memory-prune 结构化收尾应用。只有 memory-prune 收尾提示明确要求写入 daemon 提供的 result 文件时,才按该协议提交候选。`;
|
|
82
83
|
}
|
|
84
|
+
function workLogFailure(error) {
|
|
85
|
+
const reason = error instanceof Error ? error.message : "";
|
|
86
|
+
return { status: "failed", error: reason === "local_memory_work_log_source_changed"
|
|
87
|
+
|| reason === "local_memory_work_log_version_missing" ? reason : "local_memory_work_log_cleanup_failed" };
|
|
88
|
+
}
|
|
83
89
|
export async function commitLocalMemoryResultAfterExit(input) {
|
|
84
90
|
if (input.exitCode !== 0)
|
|
85
91
|
return { status: "skipped" };
|
|
@@ -97,6 +103,14 @@ export async function commitLocalMemoryResultAfterExit(input) {
|
|
|
97
103
|
if (result.outcome === "conflict" || result.outcome === "failed") {
|
|
98
104
|
return { status: "failed", error: `local_memory_result_${result.outcome}` };
|
|
99
105
|
}
|
|
106
|
+
if (input.workLogPath !== undefined) {
|
|
107
|
+
try {
|
|
108
|
+
await verifyWorkLogSource(input.agentDir, input.workLogPath, input.expectedWorkLogSha256);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
return workLogFailure(error);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
100
114
|
await applyLocalMemoryResult({
|
|
101
115
|
agentDir: input.agentDir,
|
|
102
116
|
result,
|
|
@@ -109,20 +123,12 @@ export async function commitLocalMemoryResultAfterExit(input) {
|
|
|
109
123
|
}
|
|
110
124
|
if (input.workLogPath !== undefined) {
|
|
111
125
|
try {
|
|
112
|
-
const
|
|
113
|
-
if (relativeLogPath === "" || relativeLogPath === ".." || relativeLogPath.startsWith(`..${sep}`)
|
|
114
|
-
|| isAbsolute(relativeLogPath))
|
|
115
|
-
throw new Error("local_memory_work_log_path_invalid");
|
|
116
|
-
const expectedLogPath = join(await realpath(input.agentDir), relativeLogPath);
|
|
117
|
-
const before = await lstat(input.workLogPath);
|
|
118
|
-
if (!before.isFile() || before.isSymbolicLink())
|
|
119
|
-
throw new Error("local_memory_work_log_path_invalid");
|
|
120
|
-
if (await realpath(input.workLogPath) !== expectedLogPath)
|
|
121
|
-
throw new Error("local_memory_work_log_path_invalid");
|
|
126
|
+
const before = await verifyWorkLogSource(input.agentDir, input.workLogPath, input.expectedWorkLogSha256);
|
|
122
127
|
const handle = await open(input.workLogPath, constants.O_WRONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
|
123
128
|
try {
|
|
124
129
|
const opened = await handle.stat();
|
|
125
|
-
if (!opened.isFile() || opened.dev !== before.dev || opened.ino !== before.ino
|
|
130
|
+
if (!opened.isFile() || opened.dev !== before.dev || opened.ino !== before.ino
|
|
131
|
+
|| opened.size !== before.size || opened.mtimeMs !== before.mtimeMs || opened.ctimeMs !== before.ctimeMs) {
|
|
126
132
|
throw new Error("local_memory_work_log_path_changed");
|
|
127
133
|
}
|
|
128
134
|
await handle.truncate(0);
|
|
@@ -131,8 +137,8 @@ export async function commitLocalMemoryResultAfterExit(input) {
|
|
|
131
137
|
await handle.close();
|
|
132
138
|
}
|
|
133
139
|
}
|
|
134
|
-
catch {
|
|
135
|
-
return
|
|
140
|
+
catch (error) {
|
|
141
|
+
return workLogFailure(error);
|
|
136
142
|
}
|
|
137
143
|
}
|
|
138
144
|
try {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { withKeyedLease } from "../local-execution-lease.js";
|
|
3
|
+
// 只记录尚未结束的本机执行,最后一个执行结束即移除,不落盘。
|
|
4
|
+
const activeExecutions = new Map();
|
|
5
|
+
export function workLogExecutionKey(agentsRoot, handle, taskKey) {
|
|
6
|
+
return `work-log:${JSON.stringify([resolve(agentsRoot, handle), taskKey ?? "default"])}`;
|
|
7
|
+
}
|
|
8
|
+
export async function withWorkLogExecution(key, operation, cancellation) {
|
|
9
|
+
// 只在登记期间持锁;普通任务与收尾的模型调用仍可并行。
|
|
10
|
+
await withKeyedLease(key, async () => {
|
|
11
|
+
activeExecutions.set(key, (activeExecutions.get(key) ?? 0) + 1);
|
|
12
|
+
}, cancellation);
|
|
13
|
+
try {
|
|
14
|
+
return await operation();
|
|
15
|
+
}
|
|
16
|
+
finally {
|
|
17
|
+
const remaining = (activeExecutions.get(key) ?? 1) - 1;
|
|
18
|
+
if (remaining === 0)
|
|
19
|
+
activeExecutions.delete(key);
|
|
20
|
+
else
|
|
21
|
+
activeExecutions.set(key, remaining);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export async function withWorkLogCommit(key, operation, cancellation) {
|
|
25
|
+
return withKeyedLease(key, async () => {
|
|
26
|
+
// 已有同任务执行可能继续写日志,不能提交旧提案;新执行等短提交结束后再登记。
|
|
27
|
+
if (activeExecutions.get(key) !== 1)
|
|
28
|
+
throw new Error("local_memory_work_log_execution_conflict");
|
|
29
|
+
return operation();
|
|
30
|
+
}, cancellation);
|
|
31
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { lstat, readFile, realpath } from "node:fs/promises";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { safeKey } from "../workspace.js";
|
|
5
|
+
/** 收尾会话可以换目录,但仍应消费同一任务已写好的日志;不迁移文件或复用会话。 */
|
|
6
|
+
export async function resolveMemoryPruneWorkLog(workspace, input) {
|
|
7
|
+
// 旧映射有损时无法证明是同一任务,不能用它查找或清理来源。
|
|
8
|
+
if (input.keyMode !== "opaque" || !input.taskKey || safeKey(input.taskKey) !== input.taskKey) {
|
|
9
|
+
return workspace;
|
|
10
|
+
}
|
|
11
|
+
const legacyPath = join(workspace.dir, "tasks", input.taskKey, "work-log.md");
|
|
12
|
+
if (legacyPath === workspace.workLogPath)
|
|
13
|
+
return workspace;
|
|
14
|
+
let legacyStat;
|
|
15
|
+
try {
|
|
16
|
+
legacyStat = await lstat(legacyPath);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (error.code === "ENOENT")
|
|
20
|
+
return workspace;
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
if (!legacyStat.isFile() || legacyStat.isSymbolicLink()
|
|
24
|
+
|| await realpath(legacyPath) !== join(await realpath(workspace.dir), "tasks", input.taskKey, "work-log.md")) {
|
|
25
|
+
throw new Error("local_memory_work_log_path_invalid");
|
|
26
|
+
}
|
|
27
|
+
const workLog = await readFile(legacyPath, "utf8");
|
|
28
|
+
if (workLog.length === 0)
|
|
29
|
+
return workspace;
|
|
30
|
+
if ((await readFile(workspace.workLogPath, "utf8")).length > 0) {
|
|
31
|
+
throw new Error("local_memory_work_log_source_conflict");
|
|
32
|
+
}
|
|
33
|
+
return { ...workspace, workLogPath: legacyPath, workLog };
|
|
34
|
+
}
|
|
35
|
+
export function workLogSha256(content) {
|
|
36
|
+
return createHash("sha256").update(content).digest("hex");
|
|
37
|
+
}
|
|
38
|
+
/** 调用方须在同任务的提交互斥边界内校验;指纹对应实际注入模型的那一版内容。 */
|
|
39
|
+
export async function verifyWorkLogSource(agentDir, path, expectedSha256) {
|
|
40
|
+
if (expectedSha256 === undefined)
|
|
41
|
+
throw new Error("local_memory_work_log_version_missing");
|
|
42
|
+
const relativePath = relative(resolve(agentDir), resolve(path));
|
|
43
|
+
if (relativePath === "" || relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) {
|
|
44
|
+
throw new Error("local_memory_work_log_path_invalid");
|
|
45
|
+
}
|
|
46
|
+
const before = await lstat(path);
|
|
47
|
+
if (!before.isFile() || before.isSymbolicLink()
|
|
48
|
+
|| await realpath(path) !== join(await realpath(agentDir), relativePath)) {
|
|
49
|
+
throw new Error("local_memory_work_log_path_invalid");
|
|
50
|
+
}
|
|
51
|
+
const content = await readFile(path);
|
|
52
|
+
const after = await lstat(path);
|
|
53
|
+
if (workLogSha256(content) !== expectedSha256 || before.dev !== after.dev || before.ino !== after.ino
|
|
54
|
+
|| before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs) {
|
|
55
|
+
throw new Error("local_memory_work_log_source_changed");
|
|
56
|
+
}
|
|
57
|
+
return after;
|
|
58
|
+
}
|
|
@@ -2,29 +2,12 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { lstat, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { dslog } from "../slog.js";
|
|
6
|
+
import { LocalMemoryChangeSchema, LocalMemorySingleLineText, validateLocalMemoryChange, } from "./candidate.js";
|
|
7
|
+
import { LOCAL_MEMORY_INDEX_CODE_POINT_CAP, LOCAL_MEMORY_INDEX_ENTRY_CAP, LOCAL_MEMORY_TOTAL_CODE_POINT_CAP, renderGeneratedIndex, selectLocalMemoryIndexEntries, } from "./index-budget.js";
|
|
7
8
|
import { persistPendingMemoryCandidate, readPendingMemoryCandidates, removePendingMemoryCandidate, } from "./pending.js";
|
|
8
|
-
const CategorySchema = z.enum([
|
|
9
|
-
"preferences", "project-rules", "testing", "debugging", "git", "incidents", "lessons",
|
|
10
|
-
]);
|
|
11
|
-
const SingleLineText = z.string().trim().min(1).max(500).regex(/^[^\r\n]+$/u);
|
|
12
|
-
const ChangeSchema = z.object({
|
|
13
|
-
operation: z.enum(["create", "update", "supersede"]),
|
|
14
|
-
memoryId: SingleLineText.regex(/^[^<>]+$/u).nullable(),
|
|
15
|
-
baseRevision: z.number().int().nonnegative().nullable(),
|
|
16
|
-
supersedesMemoryId: SingleLineText.regex(/^[^<>]+$/u).nullable(),
|
|
17
|
-
title: SingleLineText.max(120),
|
|
18
|
-
kind: z.enum(["procedural", "semantic", "episodic", "preference"]),
|
|
19
|
-
summary: SingleLineText,
|
|
20
|
-
triggers: z.array(SingleLineText.max(80)).min(1).max(16),
|
|
21
|
-
priority: z.enum(["high", "medium", "low"]),
|
|
22
|
-
detailCategory: CategorySchema,
|
|
23
|
-
detailBody: z.string().trim().min(1).max(12_000),
|
|
24
|
-
reviewAfter: SingleLineText.nullable(),
|
|
25
|
-
}).strict();
|
|
26
9
|
const StatusChangeSchema = z.object({
|
|
27
|
-
memoryId:
|
|
10
|
+
memoryId: LocalMemorySingleLineText,
|
|
28
11
|
expectedRevision: z.number().int().positive(),
|
|
29
12
|
expectedStatus: z.enum(["candidate", "active", "pinned", "stale", "superseded", "archived"]),
|
|
30
13
|
targetStatus: z.enum(["candidate", "active", "pinned", "stale", "archived"]),
|
|
@@ -35,7 +18,7 @@ const ResultSchema = z.object({
|
|
|
35
18
|
purpose: z.literal("memory_prune"),
|
|
36
19
|
baseCatalogRevision: z.number().int().nonnegative(),
|
|
37
20
|
outcome: z.enum(["no_change", "proposed", "conflict", "failed"]),
|
|
38
|
-
changes: z.array(
|
|
21
|
+
changes: z.array(LocalMemoryChangeSchema).max(50),
|
|
39
22
|
statusChanges: z.array(StatusChangeSchema).max(50),
|
|
40
23
|
}).strict();
|
|
41
24
|
const GENERATED_INDEX_PATTERN = /<!--\s*NOWWORK:MEMORY_INDEX:BEGIN\s+schema=1\s*-->[\s\S]*?<!--\s*NOWWORK:MEMORY_INDEX:END\s*-->/u;
|
|
@@ -43,8 +26,6 @@ const INDEX_HEADING = /^##\s+Knowledge Index\s*$/mu;
|
|
|
43
26
|
const NEXT_SECTION = /^##\s+(?!#)/mu;
|
|
44
27
|
const CARD_HEADING = /^###\s+(.+?)\s*$/gmu;
|
|
45
28
|
const FIELD_LINE = /^-\s+([a-z][a-z_]*)\s*:\s*(.*?)\s*$/iu;
|
|
46
|
-
const CONTROL_CHARACTER_PATTERN = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u;
|
|
47
|
-
const RESERVED_MEMORY_MARKER = /NOWWORK:(?:MEMORY_ENTRY|MEMORY_INDEX):/iu;
|
|
48
29
|
function codePointLength(value) {
|
|
49
30
|
return Array.from(value).length;
|
|
50
31
|
}
|
|
@@ -117,13 +98,6 @@ function parseUnmarkedMemoryIndex(memory) {
|
|
|
117
98
|
const section = indexSection(memory).replace(GENERATED_INDEX_PATTERN, "");
|
|
118
99
|
return parseCards(section);
|
|
119
100
|
}
|
|
120
|
-
function renderGeneratedIndex(entries) {
|
|
121
|
-
const cards = entries
|
|
122
|
-
.filter((entry) => entry.status === "pinned" || entry.status === "active")
|
|
123
|
-
.map(renderLocalMemoryIndexCard)
|
|
124
|
-
.join("\n");
|
|
125
|
-
return `<!-- NOWWORK:MEMORY_INDEX:BEGIN schema=1 -->\n${cards}<!-- NOWWORK:MEMORY_INDEX:END -->`;
|
|
126
|
-
}
|
|
127
101
|
function replaceGeneratedIndex(memory, entries) {
|
|
128
102
|
const generated = renderGeneratedIndex(entries);
|
|
129
103
|
if (GENERATED_INDEX_PATTERN.test(memory))
|
|
@@ -135,9 +109,15 @@ function replaceGeneratedIndex(memory, entries) {
|
|
|
135
109
|
}
|
|
136
110
|
return `${memory.trimEnd()}\n\n## Knowledge Index\n${generated}\n`;
|
|
137
111
|
}
|
|
112
|
+
function unwrappedNoteBody(title, body) {
|
|
113
|
+
const detail = body.trim();
|
|
114
|
+
const heading = /^([^\r\n]+)\r?\n\r?\n/u.exec(detail);
|
|
115
|
+
// Agent 可能回传包含包装标题的全文;只解开一层,保留内部及历史标题。
|
|
116
|
+
return heading?.[1] === `## ${title}` ? detail.slice(heading[0].length) : detail;
|
|
117
|
+
}
|
|
138
118
|
function noteSection(id, revision, title, body) {
|
|
139
119
|
return `<!-- NOWWORK:MEMORY_ENTRY:BEGIN id=${id} revision=${revision} -->\n`
|
|
140
|
-
+ `## ${title}\n\n${body
|
|
120
|
+
+ `## ${title}\n\n${unwrappedNoteBody(title, body)}\n`
|
|
141
121
|
+ `<!-- NOWWORK:MEMORY_ENTRY:END id=${id} -->\n`;
|
|
142
122
|
}
|
|
143
123
|
function notePattern(id) {
|
|
@@ -148,34 +128,12 @@ function replaceNote(content, id, replacement) {
|
|
|
148
128
|
const pattern = notePattern(id);
|
|
149
129
|
return pattern.test(content) ? content.replace(pattern, replacement) : null;
|
|
150
130
|
}
|
|
151
|
-
function containsReservedText(change) {
|
|
152
|
-
return RESERVED_MEMORY_MARKER.test([change.title, change.summary, ...change.triggers, change.detailBody].join("\n"));
|
|
153
|
-
}
|
|
154
131
|
function validateCandidate(result) {
|
|
155
132
|
if (result.outcome === "no_change" && (result.changes.length > 0 || result.statusChanges.length > 0)) {
|
|
156
133
|
throw new Error("local_memory_no_change_has_operations");
|
|
157
134
|
}
|
|
158
|
-
for (const change of result.changes)
|
|
159
|
-
|
|
160
|
-
if (containsLikelySecret(text))
|
|
161
|
-
throw new Error("local_memory_secret_candidate");
|
|
162
|
-
if (CONTROL_CHARACTER_PATTERN.test(text))
|
|
163
|
-
throw new Error("local_memory_control_character");
|
|
164
|
-
if (containsReservedText(change))
|
|
165
|
-
throw new Error("local_memory_reserved_marker");
|
|
166
|
-
if (change.operation === "create"
|
|
167
|
-
&& (change.memoryId !== null || change.baseRevision !== null || change.supersedesMemoryId !== null)) {
|
|
168
|
-
throw new Error("local_memory_create_revision_invalid");
|
|
169
|
-
}
|
|
170
|
-
if (change.operation === "update"
|
|
171
|
-
&& (change.memoryId === null || change.baseRevision === null || change.supersedesMemoryId !== null)) {
|
|
172
|
-
throw new Error("local_memory_update_revision_missing");
|
|
173
|
-
}
|
|
174
|
-
if (change.operation === "supersede"
|
|
175
|
-
&& (change.memoryId !== null || change.baseRevision === null || change.supersedesMemoryId === null)) {
|
|
176
|
-
throw new Error("local_memory_supersede_revision_missing");
|
|
177
|
-
}
|
|
178
|
-
}
|
|
135
|
+
for (const change of result.changes)
|
|
136
|
+
validateLocalMemoryChange(change);
|
|
179
137
|
}
|
|
180
138
|
export function parseLocalMemoryResult(raw) {
|
|
181
139
|
let value;
|
|
@@ -477,7 +435,16 @@ export async function applyLocalMemoryResult(input) {
|
|
|
477
435
|
// Pending create candidates are durable overflow, not dead ends. Promote only
|
|
478
436
|
// when the current catalog has room; supersede/update candidates stay pending
|
|
479
437
|
// until a fresh revision-aware decision can safely replay them.
|
|
480
|
-
const pendingCandidatesOnDisk = await readPendingMemoryCandidates({ agentDir: input.agentDir })
|
|
438
|
+
const pendingCandidatesOnDisk = await readPendingMemoryCandidates({ agentDir: input.agentDir }).catch((error) => {
|
|
439
|
+
if (!(error instanceof Error) || error.message !== "memory_pending_directory_over_budget")
|
|
440
|
+
throw error;
|
|
441
|
+
// 自动提升是可选工作,目录查询预算不应阻断本轮已验证的更新或腾位操作。
|
|
442
|
+
dslog("local_memory.pending_promotion_skipped", "pending目录超出扫描预算,本轮跳过自动提升", {
|
|
443
|
+
level: "WARN", execution_id: input.executionId, prune_trace_id: input.pruneTraceId,
|
|
444
|
+
reason: "memory_pending_directory_over_budget",
|
|
445
|
+
});
|
|
446
|
+
return [];
|
|
447
|
+
});
|
|
481
448
|
for (const pending of pendingCandidatesOnDisk) {
|
|
482
449
|
const change = pending.candidate;
|
|
483
450
|
if (change.operation !== "create")
|
|
@@ -494,8 +461,10 @@ export async function applyLocalMemoryResult(input) {
|
|
|
494
461
|
if (duplicate !== undefined) {
|
|
495
462
|
const duplicatePath = resolve(input.agentDir, duplicate.file);
|
|
496
463
|
const duplicateBody = readNoteBody(noteUpdates.get(duplicatePath) ?? await readNote(duplicatePath), duplicate.id);
|
|
497
|
-
if (duplicateBody === change.detailBody
|
|
464
|
+
if (duplicateBody === change.detailBody
|
|
465
|
+
|| duplicateBody === unwrappedNoteBody(change.title, change.detailBody).trim()) {
|
|
498
466
|
resolvedPendingIds.add(pending.pendingId);
|
|
467
|
+
}
|
|
499
468
|
continue;
|
|
500
469
|
}
|
|
501
470
|
const entry = entryFromChange(change, id, 1, "active");
|