@nowcrew/daemon 0.6.93-beta.6 → 0.6.93
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/dist/agent-environment-execution.js +7 -2
- package/dist/cloud-report-upload.js +10 -1
- package/dist/execution-journal.js +1 -3
- package/dist/execution-protocol.js +1 -1
- package/dist/execution-runner.js +1 -4
- package/dist/local-execution-prompt.js +3 -8
- package/dist/local-executor.js +37 -70
- package/dist/local-memory/index-budget.js +3 -9
- package/dist/local-memory/inventory-cli.js +7 -45
- package/dist/local-memory/inventory-source.js +8 -15
- package/dist/local-memory/prompt.js +4 -20
- package/dist/local-memory/read-cli.js +77 -75
- package/dist/local-memory/rollout.js +11 -73
- package/dist/local-memory/runtime.js +13 -176
- package/dist/local-memory/write.js +21 -135
- package/dist/local-memory-diagnostics.js +13 -94
- package/dist/local-memory-telemetry.js +2 -19
- package/dist/machine-info.js +0 -3
- package/dist/marketplace-protocol.js +1 -1
- package/dist/marketplace-remote.js +1 -1
- package/dist/marketplace-runner.js +1 -1
- package/dist/memory-prune-diagnostics.js +4 -60
- package/dist/message-file-preview.js +1 -11
- package/dist/prompt.js +2 -6
- package/dist/runner.js +1 -1
- package/dist/runtimes/claude-thread-runner.js +4 -25
- package/dist/serve.js +3 -38
- package/dist/token.js +0 -1
- package/dist/workspace-control-handler.js +3 -16
- package/dist/workspace.js +2 -13
- package/package.json +1 -1
- package/dist/local-memory/execution-workspace.js +0 -62
- package/dist/local-memory/full-index-search.js +0 -47
- package/dist/local-memory/initialization-identity.js +0 -36
- package/dist/local-memory/inventory-v2.js +0 -282
- package/dist/local-memory/legacy-adapter.js +0 -233
- package/dist/local-memory/migration-access.js +0 -196
- package/dist/local-memory/migration-cli.js +0 -54
- package/dist/local-memory/migration-io.js +0 -199
- package/dist/local-memory/migration-plan.js +0 -286
- package/dist/local-memory/migration-schema.js +0 -55
- package/dist/local-memory/migration-state-invariants.js +0 -52
- package/dist/local-memory/migration-store.js +0 -401
- package/dist/local-memory/migration-tool-entry.js +0 -21
- package/dist/local-memory/storage-format.js +0 -152
- package/dist/local-memory/writer-mode.js +0 -13
- package/dist/memory-migration-controller.js +0 -86
- package/dist/memory-migration-inspect.js +0 -108
- package/dist/memory-migration-journal.js +0 -14
- package/dist/memory-migration-protocol.js +0 -14
- package/dist/memory-migration-runtime.js +0 -305
|
@@ -6,15 +6,7 @@ import { pathToFileURL } from "node:url";
|
|
|
6
6
|
import { parseArgs, TextDecoder } from "node:util";
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import { readPendingMemoryBody, searchPendingMemoryPage } from "./pending-query.js";
|
|
9
|
-
import { detectLocalMemoryStorageVersion, parseFullMemoryIndex } from "./storage-format.js";
|
|
10
|
-
import { searchFullMemoryIndex } from "./full-index-search.js";
|
|
11
|
-
import { searchLegacyMemorySources } from "./legacy-adapter.js";
|
|
12
|
-
import { withMemoryAccess } from "./migration-access.js";
|
|
13
9
|
const OUTPUT_BYTES = 12 * 1024;
|
|
14
|
-
// A V2 page may carry coverage metadata for legacy sources. Reserve room for
|
|
15
|
-
// that metadata so a mixed result never exceeds the command's output limit.
|
|
16
|
-
const FULL_INDEX_RESULT_BYTES = 4 * 1024;
|
|
17
|
-
const FULL_INDEX_LEGACY_SCAN_BYTES = OUTPUT_BYTES - 4 * 1024;
|
|
18
10
|
const FILE_BYTES = 32 * 1024 * 1024;
|
|
19
11
|
const READ_LIMIT_MAX = 4_000;
|
|
20
12
|
const READ_LIMIT_SAFE = 2_000;
|
|
@@ -35,19 +27,11 @@ const Common = {
|
|
|
35
27
|
ifHash: z.string().regex(/^[a-f0-9]{64}$/u).optional(),
|
|
36
28
|
};
|
|
37
29
|
const CommandSchema = z.discriminatedUnion("command", [
|
|
38
|
-
z.object({
|
|
39
|
-
command: z.literal("search-history"), ...Common,
|
|
40
|
-
query: z.string().trim().min(1).max(256),
|
|
41
|
-
limit: z.coerce.number().int().min(1).max(Number.MAX_SAFE_INTEGER).default(10),
|
|
42
|
-
afterLine: z.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).default(0),
|
|
43
|
-
cursor: z.string().min(1).max(4096).optional(),
|
|
44
|
-
}).strict(),
|
|
45
30
|
z.object({
|
|
46
31
|
command: z.literal("search"), ...Common,
|
|
47
32
|
query: z.string().trim().min(1).max(256),
|
|
48
33
|
limit: z.coerce.number().int().min(1).max(Number.MAX_SAFE_INTEGER).default(10),
|
|
49
34
|
afterLine: z.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).default(0),
|
|
50
|
-
cursor: z.string().min(1).max(4096).optional(),
|
|
51
35
|
}).strict(),
|
|
52
36
|
z.object({
|
|
53
37
|
command: z.literal("read"), ...Common,
|
|
@@ -69,8 +53,7 @@ const CommandSchema = z.discriminatedUnion("command", [
|
|
|
69
53
|
}).strict(),
|
|
70
54
|
]);
|
|
71
55
|
const USAGE = `只读记忆工具,默认输出 JSON,不修改任何记忆文件。
|
|
72
|
-
crew-memory search --query <关键词> [--file MEMORY.md|notes/文件.md] [--limit 10] [--after-line 0]
|
|
73
|
-
crew-memory search-history --query <关键词> [--limit 10] [--after-line 0] [--cursor <游标>] [--if-hash <sha256>]
|
|
56
|
+
crew-memory search --query <关键词> [--file MEMORY.md|notes/文件.md] [--limit 10] [--after-line 0]
|
|
74
57
|
crew-memory read [--file MEMORY.md|notes/文件.md] [--offset 0] [--limit 2000]
|
|
75
58
|
crew-memory search-pending --query <关键词> [--cursor <游标>] [--limit 10]
|
|
76
59
|
crew-memory read-pending --id <候选ID> [--offset 0] [--limit 2000] [--if-hash <sha256>]
|
|
@@ -79,8 +62,7 @@ search 关键词按空白分隔,同一行须全部命中;返回完整文件
|
|
|
79
62
|
读取的 offset/limit 按 Unicode 字符计数;用 nextOffset 继续,eof=true 表示结束。
|
|
80
63
|
读取的 --limit 超出范围时,工具会自动降到 2000 并在结果中标记 warning;保留原文件或候选 ID 及 offset,按 nextOffset 分页,不要一次读完整文件。
|
|
81
64
|
后续页可传 --if-hash <前一页的 sha256>,文件变化时停止,避免混读不同版本。
|
|
82
|
-
|
|
83
|
-
旧格式 search/search-history 及 read 读取原文;search-history 是显式 legacy fallback,不把旧内容当作已结构化 V2 记录;read 按指定文件和哈希分页,不扫描 pending。
|
|
65
|
+
search/read 读取 MEMORY.md 和 notes 原文,不过滤失效段落,也不扫描 pending。
|
|
84
66
|
search-pending 只查因容量延期的 create 候选;有 nextCursor 就继续,即使本页 matches 为空。
|
|
85
67
|
complete=true 才表示全部候选已检索完成且无遗漏;warnings 或非零退出码表示读取未完成,不能当作零命中。
|
|
86
68
|
read-pending 只返回候选正文;offset 非零时必须传 --if-hash,update/supersede 候选不可读取。
|
|
@@ -125,6 +107,34 @@ async function readSource(home, file) {
|
|
|
125
107
|
await handle.close();
|
|
126
108
|
}
|
|
127
109
|
}
|
|
110
|
+
function characterCount(text) {
|
|
111
|
+
let count = 0;
|
|
112
|
+
for (const _character of text)
|
|
113
|
+
count += 1;
|
|
114
|
+
return count;
|
|
115
|
+
}
|
|
116
|
+
function excerpt(text, limit) {
|
|
117
|
+
let value = "";
|
|
118
|
+
let count = 0;
|
|
119
|
+
for (const character of text) {
|
|
120
|
+
if (count++ >= limit)
|
|
121
|
+
return `${value}...`;
|
|
122
|
+
value += character;
|
|
123
|
+
}
|
|
124
|
+
return value;
|
|
125
|
+
}
|
|
126
|
+
function originalLowercaseIndex(text, index) {
|
|
127
|
+
let originalIndex = 0;
|
|
128
|
+
let lowerIndex = 0;
|
|
129
|
+
for (const character of text) {
|
|
130
|
+
// 逐字符只累计小写长度;匹配仍用整行小写,保留长度不变的词尾 sigma 上下文规则。
|
|
131
|
+
lowerIndex += character.toLowerCase().length;
|
|
132
|
+
if (index < lowerIndex)
|
|
133
|
+
return originalIndex;
|
|
134
|
+
originalIndex += character.length;
|
|
135
|
+
}
|
|
136
|
+
return text.length;
|
|
137
|
+
}
|
|
128
138
|
function normalizeLimit(requestedLimit, max, safe) {
|
|
129
139
|
if (requestedLimit <= max)
|
|
130
140
|
return { limit: requestedLimit };
|
|
@@ -133,6 +143,50 @@ function normalizeLimit(requestedLimit, max, safe) {
|
|
|
133
143
|
warning: { code: "memory_limit_clamped", requestedLimit, appliedLimit: safe, recovered: true },
|
|
134
144
|
};
|
|
135
145
|
}
|
|
146
|
+
function searchSource(text, sha256, command, warning) {
|
|
147
|
+
const terms = command.query.toLowerCase().split(/\s+/u);
|
|
148
|
+
const matches = [];
|
|
149
|
+
let line = 0;
|
|
150
|
+
let offset = 0;
|
|
151
|
+
let heading = "";
|
|
152
|
+
let matchCount = 0;
|
|
153
|
+
let hasMore = false;
|
|
154
|
+
let remaining = OUTPUT_BYTES - Buffer.byteLength(JSON.stringify({ file: command.file, sha256 })) - 512;
|
|
155
|
+
for (const match of text.matchAll(/[^\n]*(?:\n|$)/gu)) {
|
|
156
|
+
if (match[0] === "")
|
|
157
|
+
break;
|
|
158
|
+
line += 1;
|
|
159
|
+
const raw = match[0];
|
|
160
|
+
const content = raw.replace(/\r?\n$/u, "");
|
|
161
|
+
const title = /^#{1,6}\s+(.+?)\s*$/u.exec(content)?.[1];
|
|
162
|
+
if (title !== undefined)
|
|
163
|
+
heading = excerpt(title, 96);
|
|
164
|
+
const lower = content.toLowerCase();
|
|
165
|
+
if (terms.every((term) => lower.includes(term))) {
|
|
166
|
+
matchCount += 1;
|
|
167
|
+
if (line > command.afterLine) {
|
|
168
|
+
let start = Math.max(0, originalLowercaseIndex(content, lower.indexOf(terms[0])) - 60);
|
|
169
|
+
if (start > 0 && /[\uDC00-\uDFFF]/u.test(content[start]))
|
|
170
|
+
start -= 1;
|
|
171
|
+
const item = { line, offset, heading, text: `${start > 0 ? "..." : ""}${excerpt(content.slice(start), 240)}` };
|
|
172
|
+
const bytes = Buffer.byteLength(JSON.stringify(item), "utf8") + 1;
|
|
173
|
+
if (hasMore || matches.length >= command.limit || bytes > remaining) {
|
|
174
|
+
hasMore = true;
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
matches.push(item);
|
|
178
|
+
remaining -= bytes;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
offset += characterCount(raw);
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
file: command.file, sha256, matches, matchCount, hasMore,
|
|
186
|
+
nextAfterLine: hasMore ? matches.at(-1)?.line ?? null : null,
|
|
187
|
+
...(warning === undefined ? {} : { warning, requestedLimit: warning.requestedLimit, limit: warning.appliedLimit }),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
136
190
|
function readPage(text, source, command, warning) {
|
|
137
191
|
const metadata = {
|
|
138
192
|
...source,
|
|
@@ -178,18 +232,6 @@ function writeResult(result, io) {
|
|
|
178
232
|
io.out(output);
|
|
179
233
|
}
|
|
180
234
|
export async function runMemoryReadCommand(argv, io) {
|
|
181
|
-
if (!io.env.CREW_HOME || argv.includes("--help") || argv.includes("-h"))
|
|
182
|
-
return runMemoryReadUnlocked(argv, io);
|
|
183
|
-
try {
|
|
184
|
-
return await withMemoryAccess(io.env.CREW_HOME, () => runMemoryReadUnlocked(argv, io));
|
|
185
|
-
}
|
|
186
|
-
catch (error) {
|
|
187
|
-
const reason = error instanceof Error && /^(?:local_)?memory_[a-z_]+$/u.test(error.message) ? error.message : "memory_migration_access_failed";
|
|
188
|
-
io.err(JSON.stringify({ error: reason, message: "记忆读取受到迁移或状态保护,不能当作没有记忆。" }));
|
|
189
|
-
return 1;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
async function runMemoryReadUnlocked(argv, io) {
|
|
193
235
|
let command;
|
|
194
236
|
try {
|
|
195
237
|
const { values, positionals } = parseArgs({
|
|
@@ -222,7 +264,6 @@ async function runMemoryReadUnlocked(argv, io) {
|
|
|
222
264
|
io.err(`memory_arguments_invalid: 参数不合法,记忆读取未完成;请按以下参数范围修正后重试,不要当作没有记忆。\n${USAGE}`);
|
|
223
265
|
return 2;
|
|
224
266
|
}
|
|
225
|
-
const fileWasExplicit = argv.some((arg) => arg === "--file" || arg.startsWith("--file="));
|
|
226
267
|
try {
|
|
227
268
|
if (!io.env.CREW_HOME)
|
|
228
269
|
throw new Error("memory_home_missing");
|
|
@@ -248,49 +289,10 @@ async function runMemoryReadUnlocked(argv, io) {
|
|
|
248
289
|
const source = await readSource(io.env.CREW_HOME, command.file);
|
|
249
290
|
if (command.ifHash !== undefined && command.ifHash !== source.sha256)
|
|
250
291
|
throw new Error("memory_file_changed");
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
const normalized = normalizeLimit(command.limit, SEARCH_LIMIT_MAX, SEARCH_LIMIT_SAFE);
|
|
254
|
-
const result = searchFullMemoryIndex(source.text, source.sha256, {
|
|
255
|
-
...command, limit: normalized.limit, history: command.command === "search-history",
|
|
256
|
-
}, FULL_INDEX_RESULT_BYTES);
|
|
257
|
-
const indexedFiles = new Set(["MEMORY.md", ...parseFullMemoryIndex(source.text).map((match) => match.file)]);
|
|
258
|
-
const legacy = await searchLegacyMemorySources({ agentDir: io.env.CREW_HOME, query: command.query,
|
|
259
|
-
limit: normalized.limit, outputBytes: FULL_INDEX_LEGACY_SCAN_BYTES,
|
|
260
|
-
...(command.cursor === undefined ? {} : { cursor: command.cursor }),
|
|
261
|
-
...(fileWasExplicit ? { files: [command.file], excludeFiles: indexedFiles } : { excludeFiles: indexedFiles }) });
|
|
262
|
-
if (result.matches.length === 0 && (command.cursor === undefined || legacy.matchCount > 0)) {
|
|
263
|
-
if (legacy.unindexedFiles.length > 0 && (legacy.matchCount > 0 || result.matchCount === 0)) {
|
|
264
|
-
writeResult({ ...legacy, ...(normalized.warning ? { warning: normalized.warning,
|
|
265
|
-
requestedLimit: normalized.warning.requestedLimit, limit: normalized.warning.appliedLimit } : {}) }, io);
|
|
266
|
-
return 0;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
writeResult({ source: "v2", legacy: false, ...result,
|
|
270
|
-
unindexedFiles: legacy.unindexedFiles,
|
|
271
|
-
unindexedFileCount: legacy.unindexedFileCount,
|
|
272
|
-
unindexedFilesTruncated: legacy.unindexedFilesTruncated,
|
|
273
|
-
complete: result.complete && legacy.matchCount === 0 && legacy.complete,
|
|
274
|
-
...(legacy.matchCount === 0 ? {} : { unindexedMatchCount: legacy.matchCount }),
|
|
275
|
-
...(legacy.matchCount === 0 ? {} : {
|
|
276
|
-
legacyHasMore: legacy.hasMore,
|
|
277
|
-
legacyNextCursor: legacy.nextCursor,
|
|
278
|
-
legacyNextAfterLine: legacy.nextAfterLine,
|
|
279
|
-
v2AfterLine: result.matches.at(-1)?.line ?? result.nextAfterLine,
|
|
280
|
-
}),
|
|
281
|
-
...(normalized.warning ? { warning: normalized.warning,
|
|
282
|
-
requestedLimit: normalized.warning.requestedLimit, limit: normalized.warning.appliedLimit } : {}) }, io);
|
|
283
|
-
return 0;
|
|
284
|
-
}
|
|
285
|
-
const result = command.command === "search" || command.command === "search-history"
|
|
286
|
-
? await (async () => {
|
|
292
|
+
const result = command.command === "search"
|
|
293
|
+
? (() => {
|
|
287
294
|
const normalized = normalizeLimit(command.limit, SEARCH_LIMIT_MAX, SEARCH_LIMIT_SAFE);
|
|
288
|
-
|
|
289
|
-
limit: normalized.limit, outputBytes: OUTPUT_BYTES - 256, afterLine: command.afterLine,
|
|
290
|
-
...(command.cursor === undefined ? {} : { cursor: command.cursor }),
|
|
291
|
-
...(fileWasExplicit ? { files: [command.file] } : {}) });
|
|
292
|
-
return { ...legacy, ...(normalized.warning ? { warning: normalized.warning,
|
|
293
|
-
requestedLimit: normalized.warning.requestedLimit, limit: normalized.warning.appliedLimit } : {}) };
|
|
295
|
+
return searchSource(source.text, source.sha256, { ...command, limit: normalized.limit }, normalized.warning);
|
|
294
296
|
})()
|
|
295
297
|
: (() => {
|
|
296
298
|
const normalized = normalizeLimit(command.limit, READ_LIMIT_MAX, READ_LIMIT_SAFE);
|
|
@@ -7,74 +7,30 @@ import { inventoryLimits, readMemoryInventorySource } from "./inventory-source.j
|
|
|
7
7
|
import { previewMemoryInventory } from "./inventory-preview.js";
|
|
8
8
|
import { parseLocalMemoryIndex } from "./write.js";
|
|
9
9
|
import { dslog } from "../slog.js";
|
|
10
|
-
import { createHash } from "node:crypto";
|
|
11
|
-
import { detectLocalMemoryStorageVersion, parseFullMemoryIndex } from "./storage-format.js";
|
|
12
|
-
import { MigrationStateSchema } from "./migration-schema.js";
|
|
13
|
-
import { assertMigrationStateIntegrity } from "./migration-state-invariants.js";
|
|
14
|
-
const TRANSIENT_INSPECTION_ERRORS = new Set([
|
|
15
|
-
"EACCES", "EPERM", "EIO", "EBUSY", "EAGAIN", "EINTR", "ESTALE", "EMFILE", "ENFILE", "ENOMEM", "ETIMEDOUT",
|
|
16
|
-
"source_changed", "memory_migration_access_busy",
|
|
17
|
-
]);
|
|
18
|
-
export function localMemoryRolloutFailure(error) {
|
|
19
|
-
const code = error?.code;
|
|
20
|
-
const reason = error instanceof Error ? error.message : "";
|
|
21
|
-
return (typeof code === "string" && TRANSIENT_INSPECTION_ERRORS.has(code))
|
|
22
|
-
|| TRANSIENT_INSPECTION_ERRORS.has(reason)
|
|
23
|
-
? "inspection_failed"
|
|
24
|
-
: "requires_migration";
|
|
25
|
-
}
|
|
26
10
|
export const LOCAL_MEMORY_V2_DEFERRED_GUIDANCE = `
|
|
27
11
|
|
|
28
12
|
## 本轮本地记忆兼容状态
|
|
29
13
|
开发环境已开放 V2,但该 Agent 的旧记忆尚未通过格式兼容检查,本轮未启用结构化 V2。
|
|
30
14
|
本节覆盖前文和旧会话中直接改写共享记忆的要求:本轮 MEMORY.md 与 notes 只读,不得改写、迁移或清空。
|
|
31
15
|
用户要求新增或更新长期规则时,内容和授权均只记录到本任务的 $CREW_TASK_LOG;保留日志,不要清空,不要宣称长期记忆已生效。旧资料兼容处理完成前,记忆收尾会明确报告需要处理。`;
|
|
32
|
-
export
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
本 Agent 尚未完成 V2 格式转换,本轮继续原记忆流程。用户明确要求新增或修改长期规则时,可按原规则最小范围更新共享记忆;记忆收尾继续依据任务日志更新原 MEMORY.md 和 notes,确认成功后再清理日志。
|
|
36
|
-
本节覆盖旧会话中因等待 V2 转换而冻结共享记忆写入的要求,但不是新的用户写入授权;本任务如明确要求不更新记忆,继续遵守。
|
|
37
|
-
保留原身份、目标及无关资料,不自行迁移或重置文件,不宣称已启用 V2。`;
|
|
38
|
-
export function reportLocalMemoryV2Deferred(executionId, handle, memoryPrune, legacyFallback = false, failure = "requires_migration") {
|
|
39
|
-
dslog("local_memory.v2_deferred", legacyFallback ? "旧格式继续原记忆流程,转换成功后再启用V2" : "记忆格式异常,保留原资料和待处理日志", {
|
|
40
|
-
execution_id: executionId, agent_handle: handle, reason: legacyFallback ? "legacy_compatible" : failure
|
|
16
|
+
export function reportLocalMemoryV2Deferred(executionId, handle, memoryPrune) {
|
|
17
|
+
dslog("local_memory.v2_deferred", "旧记忆尚未通过兼容检查,保留原资料和待处理日志", {
|
|
18
|
+
execution_id: executionId, agent_handle: handle, reason: "requires_migration"
|
|
41
19
|
});
|
|
42
|
-
if (memoryPrune
|
|
43
|
-
throw new Error(
|
|
44
|
-
}
|
|
20
|
+
if (memoryPrune)
|
|
21
|
+
throw new Error("local_memory_v2_requires_migration");
|
|
45
22
|
}
|
|
46
23
|
/** 只核对小型索引和其引用;不扫描全部历史任务、不迁移或生成新存储。 */
|
|
47
24
|
export async function resolveLocalMemoryV2Rollout(input) {
|
|
48
25
|
const environment = localMemoryV2Environment(input.serverUrl);
|
|
49
|
-
if (input.selfTraining)
|
|
50
|
-
return { enabled: false, deferred: false };
|
|
51
|
-
// 已提交的目标格式独立于新的迁移资格;关闭名单不能退回不兼容writer。
|
|
52
|
-
if (input.workspace.memory.includes("NOWWORK:MEMORY_INDEX:BEGIN schema=2")) {
|
|
53
|
-
try {
|
|
54
|
-
if (detectLocalMemoryStorageVersion(input.workspace.memory) !== 2)
|
|
55
|
-
throw new Error("invalid_storage");
|
|
56
|
-
parseFullMemoryIndex(input.workspace.memory);
|
|
57
|
-
const root = await realpath(input.workspace.dir);
|
|
58
|
-
const record = await readMemoryInventorySource(root, ".crew/memory/migration.v1.json", inventoryLimits.fileBytes);
|
|
59
|
-
const state = MigrationStateSchema.parse(JSON.parse(record.content));
|
|
60
|
-
assertMigrationStateIntegrity(state, state.identity);
|
|
61
|
-
if (state.phase !== "committed" || state.storageVersion !== 2
|
|
62
|
-
|| state.identity.agentId !== (input.agentId ?? input.handle)
|
|
63
|
-
|| state.identity.rootFingerprint !== createHash("sha256").update(root).digest("hex")
|
|
64
|
-
|| state.identity.ownerIdentity !== String((await lstat(root)).uid))
|
|
65
|
-
throw new Error("invalid_state");
|
|
66
|
-
return { enabled: true, deferred: false };
|
|
67
|
-
}
|
|
68
|
-
catch (error) {
|
|
69
|
-
return { enabled: false, deferred: true, failure: localMemoryRolloutFailure(error) };
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
if (environment === "production")
|
|
26
|
+
if (input.selfTraining || environment === "production")
|
|
73
27
|
return { enabled: false, deferred: false };
|
|
74
28
|
if (environment === "custom")
|
|
75
29
|
return { enabled: input.requested, deferred: false };
|
|
76
30
|
const deferred = { enabled: false, deferred: true };
|
|
77
31
|
const { workspace } = input;
|
|
32
|
+
if (Array.from(workspace.memory).length > LOCAL_MEMORY_TOTAL_CODE_POINT_CAP)
|
|
33
|
+
return deferred;
|
|
78
34
|
try {
|
|
79
35
|
const root = await realpath(workspace.dir);
|
|
80
36
|
const notesStat = await lstat(join(root, "notes"));
|
|
@@ -86,26 +42,8 @@ export async function resolveLocalMemoryV2Rollout(input) {
|
|
|
86
42
|
// 完整种子且无旧明细才可按空记忆接入;marker 不能替代这项核对。
|
|
87
43
|
const seededRole = /^## Role\n([\s\S]*?)\n\n## Core Goals\n/mu.exec(memory.content)?.[1];
|
|
88
44
|
if (seededRole !== undefined && memory.content === memorySeed(input.handle, seededRole)) {
|
|
89
|
-
|
|
90
|
-
&& (await readdir(join(root, "notes"))).length === 0)
|
|
91
|
-
return { enabled: true, deferred: false };
|
|
45
|
+
return (await readdir(join(root, "notes"))).length === 0 ? { enabled: true, deferred: false } : deferred;
|
|
92
46
|
}
|
|
93
|
-
if (!/NOWWORK:MEMORY_INDEX:/u.test(memory.content)) {
|
|
94
|
-
// 普通旧格式不是损坏V2。引用必须仍为根内可读文件,格式/容量例外保留原流程。
|
|
95
|
-
const entries = parseLocalMemoryIndex(memory.content);
|
|
96
|
-
// 已有受控ID或锚点的记录,不能因HTML标记丢失就退回旧writer。
|
|
97
|
-
if (entries.some(entry => !entry.id.startsWith("legacy:") || entry.anchor !== null))
|
|
98
|
-
return deferred;
|
|
99
|
-
let remaining = inventoryLimits.totalBytes - memory.bytes;
|
|
100
|
-
for (const file of new Set(entries.map(entry => entry.file))) {
|
|
101
|
-
if (!file.startsWith("notes/"))
|
|
102
|
-
return deferred;
|
|
103
|
-
remaining -= (await readMemoryInventorySource(root, file, Math.min(remaining, inventoryLimits.fileBytes))).bytes;
|
|
104
|
-
}
|
|
105
|
-
return { enabled: false, deferred: true, legacyFallback: true };
|
|
106
|
-
}
|
|
107
|
-
if (Array.from(workspace.memory).length > LOCAL_MEMORY_TOTAL_CODE_POINT_CAP)
|
|
108
|
-
return deferred;
|
|
109
47
|
const generated = /<!-- NOWWORK:MEMORY_INDEX:BEGIN schema=1 -->[\s\S]*?<!-- NOWWORK:MEMORY_INDEX:END -->/u.exec(memory.content)?.[0];
|
|
110
48
|
if (!generated || Array.from(generated).length > LOCAL_MEMORY_INDEX_CODE_POINT_CAP)
|
|
111
49
|
return deferred;
|
|
@@ -138,7 +76,7 @@ export async function resolveLocalMemoryV2Rollout(input) {
|
|
|
138
76
|
}
|
|
139
77
|
return { enabled: true, deferred: false };
|
|
140
78
|
}
|
|
141
|
-
catch
|
|
142
|
-
return
|
|
79
|
+
catch {
|
|
80
|
+
return deferred;
|
|
143
81
|
}
|
|
144
82
|
}
|
|
@@ -2,12 +2,6 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
3
|
import { lstat, mkdir, open, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import { applyMemoryMigration, prepareMemoryMigration, recoverMemoryMigration } from "./migration-store.js";
|
|
6
|
-
import { MigrationStateSchema } from "./migration-schema.js";
|
|
7
|
-
import { detectLocalMemoryStorageVersion } from "./storage-format.js";
|
|
8
|
-
import { isLocalMemoryInitializationIdentity, localMemoryInitializationIdentity } from "./initialization-identity.js";
|
|
9
|
-
import { inventory } from "./migration-io.js";
|
|
10
|
-
import { initializeExactWorkspaceSeed } from "./migration-plan.js";
|
|
11
5
|
import { applyLocalMemoryResult, parseLocalMemoryResult } from "./write.js";
|
|
12
6
|
import { verifyWorkLogSource } from "./work-log.js";
|
|
13
7
|
export function isLocalMemoryV2Eligible(input) {
|
|
@@ -18,7 +12,7 @@ export function isLocalMemoryV2Eligible(input) {
|
|
|
18
12
|
return input.enabledForHandle && input.memoryPrune;
|
|
19
13
|
}
|
|
20
14
|
const LOCAL_MEMORY_V2_MARKER = "v2-enabled";
|
|
21
|
-
async function
|
|
15
|
+
export async function initializeLocalMemoryV2(agentDir) {
|
|
22
16
|
const crewDir = join(agentDir, ".crew");
|
|
23
17
|
const memoryDir = join(crewDir, "memory");
|
|
24
18
|
await mkdir(crewDir, { recursive: true });
|
|
@@ -37,149 +31,24 @@ async function writeLocalMemoryV2Marker(agentDir) {
|
|
|
37
31
|
throw error;
|
|
38
32
|
}
|
|
39
33
|
}
|
|
40
|
-
function
|
|
41
|
-
return {
|
|
42
|
-
...identity,
|
|
43
|
-
kind: "server_write_intent",
|
|
44
|
-
sourceDigest: state.sourceDigest,
|
|
45
|
-
planDigest: state.planDigest,
|
|
46
|
-
intentVersion: 1,
|
|
47
|
-
// New-Agent initialization is derived from the exact pristine seed. Keep
|
|
48
|
-
// the receipt deterministic so a crash between intent publication and the
|
|
49
|
-
// mutable state update can be retried without creating a conflicting fact.
|
|
50
|
-
confirmedAt: "1970-01-01T00:00:00.000Z",
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Initialize a pristine new Agent through the same journaled schema2
|
|
55
|
-
* publication path used by migration. The marker is written only after the
|
|
56
|
-
* committed catalog and state have both been verified.
|
|
57
|
-
*/
|
|
58
|
-
async function initializeLocalMemoryV2WithBinding(agentDir, agentId, binding) {
|
|
59
|
-
const identity = await localMemoryInitializationIdentity(agentDir, agentId, binding);
|
|
60
|
-
let state = await prepareMemoryMigration({
|
|
61
|
-
agentDir,
|
|
62
|
-
identity,
|
|
63
|
-
serverHistory: { kind: "none", isNewAgent: true },
|
|
64
|
-
});
|
|
65
|
-
if (state.origin !== "initialized")
|
|
66
|
-
throw new Error("local_memory_v2_initialization_source_invalid");
|
|
67
|
-
if (state.phase === "prepared") {
|
|
68
|
-
state = await applyMemoryMigration({
|
|
69
|
-
agentDir,
|
|
70
|
-
identity,
|
|
71
|
-
intentReceipt: initializedIntent(identity, state),
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
else if (state.phase === "applying" || state.phase === "repair_required") {
|
|
75
|
-
state = await recoverMemoryMigration({
|
|
76
|
-
agentDir,
|
|
77
|
-
identity,
|
|
78
|
-
intentReceipt: state.intentReceipt ?? initializedIntent(identity, state),
|
|
79
|
-
stoppedReceipt: {
|
|
80
|
-
kind: "migration_process_stopped",
|
|
81
|
-
migrationId: identity.migrationId,
|
|
82
|
-
machineId: identity.machineId,
|
|
83
|
-
executionId: identity.executionId,
|
|
84
|
-
ownerIdentity: identity.ownerIdentity,
|
|
85
|
-
pid: null,
|
|
86
|
-
stoppedAt: new Date().toISOString(),
|
|
87
|
-
proof: "journal_reconciled",
|
|
88
|
-
},
|
|
89
|
-
cancelled: false,
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
const memory = await readFile(join(agentDir, "MEMORY.md"), "utf8");
|
|
93
|
-
if (state.phase !== "committed" || state.origin !== "initialized" || state.storageVersion !== 2
|
|
94
|
-
|| detectLocalMemoryStorageVersion(memory) !== 2) {
|
|
95
|
-
throw new Error("local_memory_v2_initialization_incomplete");
|
|
96
|
-
}
|
|
97
|
-
await writeLocalMemoryV2Marker(agentDir);
|
|
98
|
-
}
|
|
99
|
-
/** Fresh initialization requires the identity from the authenticated ready frame. */
|
|
100
|
-
export function initializeLocalMemoryV2(agentDir, agentId, binding) {
|
|
101
|
-
return initializeLocalMemoryV2WithBinding(agentDir, agentId, binding);
|
|
102
|
-
}
|
|
103
|
-
export async function initializeLocalMemoryV2ForWorkspace(workspace, enabledForHandle, agentId, binding) {
|
|
34
|
+
export async function initializeLocalMemoryV2ForWorkspace(workspace, enabledForHandle) {
|
|
104
35
|
const initialized = workspace.localMemoryV2Initialized === true;
|
|
105
|
-
if (initialized
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
// callers side-effect free instead of probing an unrelated filesystem path.
|
|
109
|
-
if (workspace.localMemoryV2Initialized !== false)
|
|
110
|
-
return false;
|
|
111
|
-
let resumable = false;
|
|
112
|
-
let effectiveBinding = binding;
|
|
36
|
+
if (!enabledForHandle || initialized || workspace.localMemoryV2Initialized !== false
|
|
37
|
+
|| workspace.agentHomePreexisted === true || workspace.memorySeedCreated !== true)
|
|
38
|
+
return initialized;
|
|
113
39
|
try {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const legacyIdentity = binding === undefined
|
|
117
|
-
? identity
|
|
118
|
-
: await localMemoryInitializationIdentity(workspace.dir, agentId);
|
|
119
|
-
resumable = state.origin === "initialized"
|
|
120
|
-
&& state.phase !== "rolled_back"
|
|
121
|
-
&& (isLocalMemoryInitializationIdentity(state.identity, identity)
|
|
122
|
-
|| isLocalMemoryInitializationIdentity(state.identity, legacyIdentity));
|
|
123
|
-
if (resumable && isLocalMemoryInitializationIdentity(state.identity, legacyIdentity))
|
|
124
|
-
effectiveBinding = undefined;
|
|
125
|
-
}
|
|
126
|
-
catch (error) {
|
|
127
|
-
if (error.code !== "ENOENT")
|
|
128
|
-
throw error;
|
|
129
|
-
try {
|
|
130
|
-
const migrations = await lstat(join(workspace.dir, ".crew", "memory", "migrations"));
|
|
131
|
-
if (!migrations.isDirectory() || migrations.isSymbolicLink()) {
|
|
132
|
-
throw new Error("local_memory_v2_initialization_residue_invalid");
|
|
133
|
-
}
|
|
134
|
-
const identity = await localMemoryInitializationIdentity(workspace.dir, agentId, binding);
|
|
135
|
-
const legacyIdentity = binding === undefined
|
|
136
|
-
? identity
|
|
137
|
-
: await localMemoryInitializationIdentity(workspace.dir, agentId);
|
|
138
|
-
let operationIdentity = identity;
|
|
139
|
-
const operation = await lstat(join(workspace.dir, ".crew", "memory", "migrations", identity.migrationId))
|
|
140
|
-
.catch(async (error) => {
|
|
141
|
-
if (error.code !== "ENOENT" || identity.migrationId === legacyIdentity.migrationId)
|
|
142
|
-
throw error;
|
|
143
|
-
operationIdentity = legacyIdentity;
|
|
144
|
-
return lstat(join(workspace.dir, ".crew", "memory", "migrations", legacyIdentity.migrationId));
|
|
145
|
-
});
|
|
146
|
-
if (!operation.isDirectory() || operation.isSymbolicLink()) {
|
|
147
|
-
throw new Error("local_memory_v2_initialization_residue_invalid");
|
|
148
|
-
}
|
|
149
|
-
const current = await inventory(workspace.dir);
|
|
150
|
-
const memory = current.files.get("MEMORY.md");
|
|
151
|
-
resumable = current.files.size === 1 && memory !== undefined
|
|
152
|
-
&& current.directories.every(path => path === "notes" || path === ".crew/memory/pending")
|
|
153
|
-
&& initializeExactWorkspaceSeed(memory) !== null;
|
|
154
|
-
if (resumable && operationIdentity.migrationId === legacyIdentity.migrationId)
|
|
155
|
-
effectiveBinding = undefined;
|
|
156
|
-
}
|
|
157
|
-
catch (residueError) {
|
|
158
|
-
if (residueError.code !== "ENOENT")
|
|
159
|
-
throw residueError;
|
|
160
|
-
}
|
|
40
|
+
await initializeLocalMemoryV2(workspace.dir);
|
|
41
|
+
return true;
|
|
161
42
|
}
|
|
162
|
-
|
|
163
|
-
&& workspace.localMemoryV2Initialized === false
|
|
164
|
-
&& workspace.agentHomePreexisted !== true
|
|
165
|
-
&& workspace.memorySeedCreated === true;
|
|
166
|
-
if (!resumable && !pristineFirstInitialization)
|
|
167
|
-
return false;
|
|
168
|
-
// Never create another unbound sentinel. Undefined is retained solely to
|
|
169
|
-
// finish a pre-upgrade journal already proven above.
|
|
170
|
-
if (!resumable && effectiveBinding === undefined)
|
|
43
|
+
catch {
|
|
171
44
|
return false;
|
|
172
|
-
|
|
173
|
-
return true;
|
|
45
|
+
}
|
|
174
46
|
}
|
|
175
47
|
export function localMemoryResultPath(runDir, executionId) {
|
|
176
48
|
const suffix = createHash("sha256").update(executionId, "utf8").digest("hex").slice(0, 32);
|
|
177
49
|
return join(runDir, `.memory-result.v1-${suffix}.json`);
|
|
178
50
|
}
|
|
179
|
-
export function renderLocalMemoryResultInstructions(resultPath
|
|
180
|
-
const storagePolicy = storageVersion === 2
|
|
181
|
-
? "完整目录保存在 MEMORY.md,模型只看到有界预览;未展示记录仍然有效。涉及旧规则时先用 crew-memory search 查询完整目录,按当前 ID/revision 更新,不因预览未命中就重复新建。20条预览不是有效记录上限;完整存储超出资源上限会拒绝提交,不得自行删除或降级旧记录。已有 pending 仍是未应用候选,须重新核对授权、重复与版本后通过 changes 申请保存,不因迁移自动生效。"
|
|
182
|
-
: "daemon 会校验、去重、限制 MEMORY.md 大小,超出索引容量的候选会保存在 pending 归档中。容量暂存的 create 可按需检索参考但不能覆盖现行规则;有空间的后续 proposed 提交可提升。未应用的 update/supersede 不进入普通召回,必须重新核对目标版本后处理。";
|
|
51
|
+
export function renderLocalMemoryResultInstructions(resultPath) {
|
|
183
52
|
return `
|
|
184
53
|
|
|
185
54
|
## Structured memory prune (daemon-managed)
|
|
@@ -201,7 +70,7 @@ outcome 只允许 no_change、proposed、conflict、failed。新增、更新、
|
|
|
201
70
|
\`\`\`
|
|
202
71
|
|
|
203
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。
|
|
204
|
-
只提交可复用、与本次任务明确相关的少量候选;不要写入凭证、token
|
|
73
|
+
只提交可复用、与本次任务明确相关的少量候选;不要写入凭证、token、隐私或内部控制标记。daemon 会校验、去重、限制 MEMORY.md 大小,超出索引容量的候选会保存在 pending 归档中。容量暂存的 create 可按需检索参考但不能覆盖现行规则;有空间的后续 proposed 提交可提升。未应用的 update/supersede 不进入普通召回,必须重新核对目标版本后处理。
|
|
205
74
|
写入候选文件只表示提交候选,不能提前宣称已经记住或规则已经更新;daemon 在本轮结束后才处理,候选持久化与进入有效索引也不是同一结果。本结构化轮次覆盖前文提前清理工作日志的要求:保留 $CREW_TASK_LOG,不要清空或删除;daemon 将在处理成功后清理,失败时保留日志和候选用于排查或重试。
|
|
206
75
|
`;
|
|
207
76
|
}
|
|
@@ -212,20 +81,6 @@ export function renderLocalMemoryV2WriteGuard() {
|
|
|
212
81
|
## V2 shared memory write safety
|
|
213
82
|
本节覆盖前文关于普通任务直接更新共享记忆的例外规则。本 Agent 已启用结构化本地记忆 V2。普通任务不得直接编辑共享的 $CREW_HOME/MEMORY.md 或 $CREW_HOME/notes/ 文件;即使用户明确要求记住内容,也只把候选、用户授权和分类理由记录到本任务的 $CREW_TASK_LOG,留给任务结束后的 memory-prune 结构化收尾应用。只有 memory-prune 收尾提示明确要求写入 daemon 提供的 result 文件时,才按该协议提交候选。`;
|
|
214
83
|
}
|
|
215
|
-
async function localMemoryStorageFacts(agentDir) {
|
|
216
|
-
const memory = await readFile(join(agentDir, "MEMORY.md"), "utf8");
|
|
217
|
-
const storageVersion = detectLocalMemoryStorageVersion(memory);
|
|
218
|
-
let committedState = false;
|
|
219
|
-
try {
|
|
220
|
-
const state = MigrationStateSchema.parse(JSON.parse(await readFile(join(agentDir, ".crew", "memory", "migration.v1.json"), "utf8")));
|
|
221
|
-
committedState = state.phase === "committed" && state.storageVersion === 2;
|
|
222
|
-
}
|
|
223
|
-
catch (error) {
|
|
224
|
-
if (error.code !== "ENOENT" && storageVersion === 2)
|
|
225
|
-
throw error;
|
|
226
|
-
}
|
|
227
|
-
return { storageVersion, committedState };
|
|
228
|
-
}
|
|
229
84
|
function workLogFailure(error) {
|
|
230
85
|
const reason = error instanceof Error ? error.message : "";
|
|
231
86
|
return { status: "failed", error: reason === "local_memory_work_log_source_changed"
|
|
@@ -243,9 +98,6 @@ export async function commitLocalMemoryResultAfterExit(input) {
|
|
|
243
98
|
return { status: "missing" };
|
|
244
99
|
return { status: "failed", error: error instanceof Error ? error.message : "read_failed" };
|
|
245
100
|
}
|
|
246
|
-
let storageBefore;
|
|
247
|
-
let storageAfter;
|
|
248
|
-
let applied;
|
|
249
101
|
try {
|
|
250
102
|
const result = parseLocalMemoryResult(raw);
|
|
251
103
|
if (result.outcome === "conflict" || result.outcome === "failed") {
|
|
@@ -259,24 +111,12 @@ export async function commitLocalMemoryResultAfterExit(input) {
|
|
|
259
111
|
return workLogFailure(error);
|
|
260
112
|
}
|
|
261
113
|
}
|
|
262
|
-
|
|
263
|
-
if (input.requiredStorageVersion === 2
|
|
264
|
-
&& (storageBefore.storageVersion !== 2 || !storageBefore.committedState)) {
|
|
265
|
-
return { status: "failed", error: "local_memory_v2_state_invalid_before_commit" };
|
|
266
|
-
}
|
|
267
|
-
applied = await applyLocalMemoryResult({
|
|
114
|
+
await applyLocalMemoryResult({
|
|
268
115
|
agentDir: input.agentDir,
|
|
269
116
|
result,
|
|
270
117
|
executionId: input.executionId,
|
|
271
118
|
pruneTraceId: input.pruneTraceId,
|
|
272
|
-
...(input.writerCapabilities === undefined ? {} : { writerCapabilities: input.writerCapabilities }),
|
|
273
|
-
...(input.requiredStorageVersion === undefined ? {} : { requiredStorageVersion: input.requiredStorageVersion }),
|
|
274
119
|
});
|
|
275
|
-
storageAfter = await localMemoryStorageFacts(input.agentDir);
|
|
276
|
-
if (input.requiredStorageVersion === 2
|
|
277
|
-
&& (storageAfter.storageVersion !== 2 || !storageAfter.committedState)) {
|
|
278
|
-
return { status: "failed", error: "local_memory_v2_state_invalid_after_commit" };
|
|
279
|
-
}
|
|
280
120
|
}
|
|
281
121
|
catch (error) {
|
|
282
122
|
return { status: "failed", error: error instanceof Error ? error.message : "apply_failed" };
|
|
@@ -307,8 +147,5 @@ export async function commitLocalMemoryResultAfterExit(input) {
|
|
|
307
147
|
catch {
|
|
308
148
|
return { status: "failed", error: "local_memory_result_cleanup_failed" };
|
|
309
149
|
}
|
|
310
|
-
return { status: "applied"
|
|
311
|
-
storageVersionAfter: storageAfter.storageVersion, committedState: storageAfter.committedState,
|
|
312
|
-
createdIds: applied.createdIds, updatedIds: applied.updatedIds,
|
|
313
|
-
statusChangedIds: applied.statusChangedIds, pendingIds: applied.pendingIds };
|
|
150
|
+
return { status: "applied" };
|
|
314
151
|
}
|