@nowcrew/daemon 0.6.101-beta.2 → 0.6.101-beta.4
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/cloud-report-upload.js +10 -1
- package/dist/computer-main.js +0 -0
- package/dist/execution-journal-lock.js +51 -2
- package/dist/execution-journal.js +1 -3
- package/dist/execution-protocol.js +1 -1
- package/dist/execution-runner.js +1 -4
- package/dist/host-execution-coordinator.js +10 -0
- package/dist/local-execution-prompt.js +3 -8
- package/dist/local-executor.js +55 -78
- package/dist/local-memory/index-budget.js +3 -9
- package/dist/local-memory/inventory-cli.js +8 -52
- package/dist/local-memory/inventory-source.js +8 -15
- package/dist/local-memory/pending-query.js +2 -5
- package/dist/local-memory/pending-schema.js +1 -1
- package/dist/local-memory/pending.js +2 -16
- package/dist/local-memory/prompt.js +5 -21
- package/dist/local-memory/read-cli.js +76 -98
- package/dist/local-memory/rollout.js +11 -79
- package/dist/local-memory/runtime.js +17 -191
- package/dist/local-memory/write.js +21 -190
- package/dist/local-memory-diagnostics.js +15 -96
- package/dist/local-memory-telemetry.js +3 -20
- package/dist/machine-info.js +3 -4
- package/dist/machine-runtime-environment.js +1 -0
- package/dist/main.js +0 -0
- 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 -74
- package/dist/message-file-preview.js +1 -11
- package/dist/prompt.js +17 -9
- package/dist/runner.js +1 -1
- package/dist/runtime-tool-update-bridge.js +126 -0
- package/dist/runtime-tool-update-controller.js +151 -0
- package/dist/runtime-tool-update-protocol.js +24 -0
- package/dist/runtime-tool-update-serve.js +54 -0
- package/dist/runtimes/claude-thread-runner.js +4 -25
- package/dist/serve.js +12 -38
- package/dist/token.js +0 -1
- package/dist/workspace-control-handler.js +3 -16
- package/dist/workspace.js +2 -13
- package/package.json +10 -9
- package/dist/local-memory/commit-telemetry.js +0 -30
- 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 -362
- package/dist/local-memory/legacy-adapter.js +0 -233
- package/dist/local-memory/migration-access.js +0 -242
- package/dist/local-memory/migration-cli.js +0 -54
- package/dist/local-memory/migration-io.js +0 -199
- package/dist/local-memory/migration-pending.js +0 -9
- package/dist/local-memory/migration-plan.js +0 -432
- package/dist/local-memory/migration-schema.js +0 -55
- package/dist/local-memory/migration-state-invariants.js +0 -65
- package/dist/local-memory/migration-store.js +0 -414
- package/dist/local-memory/migration-tool-entry.js +0 -21
- package/dist/local-memory/release-artifact-guard.js +0 -55
- package/dist/local-memory/source-path.js +0 -13
- package/dist/local-memory/storage-format.js +0 -249
- package/dist/local-memory/writer-mode.js +0 -13
- package/dist/memory-migration-controller.js +0 -89
- package/dist/memory-migration-inspect.js +0 -116
- package/dist/memory-migration-journal.js +0 -14
- package/dist/memory-migration-protocol.js +0 -14
- package/dist/memory-migration-runtime.js +0 -305
|
@@ -15,7 +15,6 @@ const SearchSchema = z.object({
|
|
|
15
15
|
cursor: z.string().max(2048).optional(), limit: z.number().int().min(1).max(40).default(10),
|
|
16
16
|
}).strict();
|
|
17
17
|
const excerpt = (text, cap) => Array.from(text).slice(0, cap).join("");
|
|
18
|
-
const pendingState = (reason) => reason === "legacy_requires_migration" ? "migration_deferred" : "capacity_deferred";
|
|
19
18
|
export async function searchPendingMemoryPage(input) {
|
|
20
19
|
const parsed = SearchSchema.safeParse(input);
|
|
21
20
|
if (!parsed.success)
|
|
@@ -62,8 +61,7 @@ export async function searchPendingMemoryPage(input) {
|
|
|
62
61
|
const match = source && isRecallablePending(source.candidate) && pendingRecallScore(source.candidate, args.query) > 0
|
|
63
62
|
? { pendingId: source.candidate.pendingId, sha256: source.sha256, title: source.candidate.candidate.title,
|
|
64
63
|
summary: excerpt(source.candidate.candidate.summary, 180), triggers: source.candidate.candidate.triggers.slice(0, 4),
|
|
65
|
-
operation: "create",
|
|
66
|
-
state: pendingState(source.candidate.reason) }
|
|
64
|
+
operation: "create", state: "capacity_deferred" }
|
|
67
65
|
: null;
|
|
68
66
|
const addition = match ?? warning;
|
|
69
67
|
const size = addition ? Buffer.byteLength(JSON.stringify(addition), "utf8") + 2 : 0;
|
|
@@ -98,6 +96,5 @@ export async function readPendingMemoryBody(input) {
|
|
|
98
96
|
throw new Error("memory_pending_not_recallable");
|
|
99
97
|
const candidate = source.candidate.candidate;
|
|
100
98
|
return { pendingId: source.candidate.pendingId, sha256: source.sha256, title: candidate.title,
|
|
101
|
-
operation: "create",
|
|
102
|
-
state: pendingState(source.candidate.reason), content: candidate.detailBody };
|
|
99
|
+
operation: "create", state: "capacity_deferred", content: candidate.detailBody };
|
|
103
100
|
}
|
|
@@ -8,7 +8,7 @@ const ArchivedMemoryChangeSchema = LocalMemoryChangeSchema.extend({
|
|
|
8
8
|
export const PendingIdSchema = z.string().max(200).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u);
|
|
9
9
|
export const PendingMemorySchema = z.object({
|
|
10
10
|
schemaVersion: z.literal(1), pendingId: PendingIdSchema,
|
|
11
|
-
createdAt: z.string().min(1).max(100), reason: z.
|
|
11
|
+
createdAt: z.string().min(1).max(100), reason: z.literal("index_over_budget"),
|
|
12
12
|
source: z.object({ executionId: z.string().min(1).max(500), pruneTraceId: z.string().min(1).max(500).nullable() }).strict(),
|
|
13
13
|
candidate: ArchivedMemoryChangeSchema,
|
|
14
14
|
}).strict();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { lstat, mkdir, realpath, rename, rm,
|
|
2
|
+
import { lstat, mkdir, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { isRecallablePending, parsePendingMemoryCandidate } from "./pending-schema.js";
|
|
5
5
|
import { pendingFileNames, pendingReadDirectory, PENDING_FILE_BYTES, readPendingSource } from "./pending-source.js";
|
|
@@ -103,17 +103,6 @@ export async function persistPendingMemoryCandidate(input) {
|
|
|
103
103
|
const directory = await pendingDirectory(input.agentDir);
|
|
104
104
|
const target = join(directory, `${parsed.pendingId}.json`);
|
|
105
105
|
await assertRegularFile(target);
|
|
106
|
-
try {
|
|
107
|
-
const existing = parsePendingMemoryCandidate(JSON.parse(await readFile(target, "utf8")));
|
|
108
|
-
const dedupKey = (value) => value.reason === "legacy_requires_migration"
|
|
109
|
-
? JSON.stringify({ schemaVersion: value.schemaVersion, reason: value.reason, candidate: value.candidate })
|
|
110
|
-
: JSON.stringify({ ...value, createdAt: "" });
|
|
111
|
-
if (dedupKey(existing) === dedupKey(parsed))
|
|
112
|
-
return target;
|
|
113
|
-
}
|
|
114
|
-
catch {
|
|
115
|
-
// A malformed existing candidate is replaced by the validated input below.
|
|
116
|
-
}
|
|
117
106
|
await atomicWrite(target, `${JSON.stringify(parsed, null, 2)}\n`);
|
|
118
107
|
return target;
|
|
119
108
|
}
|
|
@@ -175,10 +164,7 @@ function truncateCodePoints(value, maxCodePoints) {
|
|
|
175
164
|
return Array.from(value).slice(0, Math.max(0, maxCodePoints)).join("");
|
|
176
165
|
}
|
|
177
166
|
export function renderPendingMemoryContext(candidates, maxCodePoints = 2_000) {
|
|
178
|
-
|
|
179
|
-
// be injected into the ordinary prompt before a reviewed migration promotes
|
|
180
|
-
// them to the active catalog.
|
|
181
|
-
const recallable = candidates.filter((candidate) => isRecallablePending(candidate) && candidate.reason === "index_over_budget");
|
|
167
|
+
const recallable = candidates.filter(isRecallablePending);
|
|
182
168
|
if (recallable.length === 0 || maxCodePoints <= 0)
|
|
183
169
|
return "";
|
|
184
170
|
const header = `\n\n## Archived memory candidates (daemon-managed)\n容量暂存的新增资料,仅为部分预览,不代表完整检索;可参考但不能覆盖现行规则,未包含内容请按需查询。\n`;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { dslog } from "../slog.js";
|
|
2
2
|
import { renderPendingMemoryContext, searchPendingMemoryCandidates } from "./pending.js";
|
|
3
3
|
import { renderLocalMemoryResultInstructions, renderLocalMemoryV2WriteGuard } from "./runtime.js";
|
|
4
|
-
import { LOCAL_MEMORY_V2_DEFERRED_GUIDANCE
|
|
5
|
-
const PENDING_READ_GUIDANCE = "\n\nV2新增资料预览未命中不代表没有资料;先查有效索引,必要时用 crew-memory search-pending --query '关键词',按 nextCursor 续查;用 read-pending --id ID
|
|
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";
|
|
6
6
|
export async function prepareLocalMemoryPrompt(input) {
|
|
7
7
|
let pending = "";
|
|
8
8
|
if (input.enabled) {
|
|
@@ -22,11 +22,10 @@ export async function prepareLocalMemoryPrompt(input) {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
const original = input.renderBase(0);
|
|
25
|
-
const guard = input.
|
|
25
|
+
const guard = input.deferred ? LOCAL_MEMORY_V2_DEFERRED_GUIDANCE
|
|
26
26
|
: input.strictRollout && input.enabled && input.resultPath === null ? renderLocalMemoryV2WriteGuard() : "";
|
|
27
|
-
const structured = input.resultPath === null ? "" : renderLocalMemoryResultInstructions(input.resultPath
|
|
27
|
+
const structured = input.resultPath === null ? "" : renderLocalMemoryResultInstructions(input.resultPath);
|
|
28
28
|
const guidance = input.enabled ? PENDING_READ_GUIDANCE : "";
|
|
29
|
-
let requiredMemoryBudgetError;
|
|
30
29
|
// 短命令指引放在当前轮,独立于可舍弃预览;收尾契约仍保持系统与当前轮两份。
|
|
31
30
|
for (const choice of [
|
|
32
31
|
{ suffix: pending + guard + structured, wake: guard + structured + guidance },
|
|
@@ -34,26 +33,11 @@ export async function prepareLocalMemoryPrompt(input) {
|
|
|
34
33
|
{ suffix: guard + structured, wake: guard + structured },
|
|
35
34
|
]) {
|
|
36
35
|
const reserved = Buffer.byteLength(choice.suffix + choice.wake, "utf8");
|
|
37
|
-
|
|
38
|
-
try {
|
|
39
|
-
base = reserved === 0 ? original : input.renderBase(reserved);
|
|
40
|
-
}
|
|
41
|
-
catch (error) {
|
|
42
|
-
// 可选候选先让出预算;最后仍不能容纳身份/置顶时必须拒绝,不能省略必需记忆。
|
|
43
|
-
if (input.storageVersion === 2 && error instanceof Error
|
|
44
|
-
&& ["local_memory_identity_budget_exceeded", "local_memory_pinned_budget_exceeded"].includes(error.message)) {
|
|
45
|
-
requiredMemoryBudgetError = error;
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
throw error;
|
|
49
|
-
}
|
|
50
|
-
const systemPrompt = base + choice.suffix;
|
|
36
|
+
const systemPrompt = (reserved === 0 ? original : input.renderBase(reserved)) + choice.suffix;
|
|
51
37
|
if (input.maxBytes === undefined || Buffer.byteLength(systemPrompt + choice.wake, "utf8") <= input.maxBytes) {
|
|
52
38
|
return { systemPrompt, resultPath: input.resultPath, wakePromptSuffix: choice.wake };
|
|
53
39
|
}
|
|
54
40
|
}
|
|
55
|
-
if (requiredMemoryBudgetError)
|
|
56
|
-
throw requiredMemoryBudgetError;
|
|
57
41
|
if (input.strictRollout)
|
|
58
42
|
throw new Error("local_memory_v2_prompt_over_budget");
|
|
59
43
|
// 强制基础指令与契约本身仍无法共存时,保留既有非结构化兼容回退,不裁基础指令。
|
|
@@ -6,18 +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
|
-
import { readMemoryInventorySource, scanMemorySources } from "./inventory-source.js";
|
|
14
|
-
import { MigrationStateSchema } from "./migration-schema.js";
|
|
15
|
-
import { findControlledPreservedSourcePaths } from "./inventory-v2.js";
|
|
16
9
|
const OUTPUT_BYTES = 12 * 1024;
|
|
17
|
-
// A V2 page may carry coverage metadata for legacy sources. Reserve room for
|
|
18
|
-
// that metadata so a mixed result never exceeds the command's output limit.
|
|
19
|
-
const FULL_INDEX_RESULT_BYTES = 4 * 1024;
|
|
20
|
-
const FULL_INDEX_LEGACY_SCAN_BYTES = OUTPUT_BYTES - 4 * 1024;
|
|
21
10
|
const FILE_BYTES = 32 * 1024 * 1024;
|
|
22
11
|
const READ_LIMIT_MAX = 4_000;
|
|
23
12
|
const READ_LIMIT_SAFE = 2_000;
|
|
@@ -38,19 +27,11 @@ const Common = {
|
|
|
38
27
|
ifHash: z.string().regex(/^[a-f0-9]{64}$/u).optional(),
|
|
39
28
|
};
|
|
40
29
|
const CommandSchema = z.discriminatedUnion("command", [
|
|
41
|
-
z.object({
|
|
42
|
-
command: z.literal("search-history"), ...Common,
|
|
43
|
-
query: z.string().trim().min(1).max(256),
|
|
44
|
-
limit: z.coerce.number().int().min(1).max(Number.MAX_SAFE_INTEGER).default(10),
|
|
45
|
-
afterLine: z.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).default(0),
|
|
46
|
-
cursor: z.string().min(1).max(4096).optional(),
|
|
47
|
-
}).strict(),
|
|
48
30
|
z.object({
|
|
49
31
|
command: z.literal("search"), ...Common,
|
|
50
32
|
query: z.string().trim().min(1).max(256),
|
|
51
33
|
limit: z.coerce.number().int().min(1).max(Number.MAX_SAFE_INTEGER).default(10),
|
|
52
34
|
afterLine: z.coerce.number().int().min(0).max(Number.MAX_SAFE_INTEGER).default(0),
|
|
53
|
-
cursor: z.string().min(1).max(4096).optional(),
|
|
54
35
|
}).strict(),
|
|
55
36
|
z.object({
|
|
56
37
|
command: z.literal("read"), ...Common,
|
|
@@ -72,8 +53,7 @@ const CommandSchema = z.discriminatedUnion("command", [
|
|
|
72
53
|
}).strict(),
|
|
73
54
|
]);
|
|
74
55
|
const USAGE = `只读记忆工具,默认输出 JSON,不修改任何记忆文件。
|
|
75
|
-
crew-memory search --query <关键词> [--file MEMORY.md|notes/文件.md] [--limit 10] [--after-line 0]
|
|
76
|
-
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]
|
|
77
57
|
crew-memory read [--file MEMORY.md|notes/文件.md] [--offset 0] [--limit 2000]
|
|
78
58
|
crew-memory search-pending --query <关键词> [--cursor <游标>] [--limit 10]
|
|
79
59
|
crew-memory read-pending --id <候选ID> [--offset 0] [--limit 2000] [--if-hash <sha256>]
|
|
@@ -82,12 +62,11 @@ search 关键词按空白分隔,同一行须全部命中;返回完整文件
|
|
|
82
62
|
读取的 offset/limit 按 Unicode 字符计数;用 nextOffset 继续,eof=true 表示结束。
|
|
83
63
|
读取的 --limit 超出范围时,工具会自动降到 2000 并在结果中标记 warning;保留原文件或候选 ID 及 offset,按 nextOffset 分页,不要一次读完整文件。
|
|
84
64
|
后续页可传 --if-hash <前一页的 sha256>,文件变化时停止,避免混读不同版本。
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
search-pending 只查容量延期或迁移延期的 create 候选;有 nextCursor 就继续,即使本页 matches 为空。
|
|
65
|
+
search/read 读取 MEMORY.md 和 notes 原文,不过滤失效段落,也不扫描 pending。
|
|
66
|
+
search-pending 只查因容量延期的 create 候选;有 nextCursor 就继续,即使本页 matches 为空。
|
|
88
67
|
complete=true 才表示全部候选已检索完成且无遗漏;warnings 或非零退出码表示读取未完成,不能当作零命中。
|
|
89
68
|
read-pending 只返回候选正文;offset 非零时必须传 --if-hash,update/supersede 候选不可读取。
|
|
90
|
-
|
|
69
|
+
候选只供相关问题参考,未生效,不能覆盖有效规则;先读取当前有效索引与明细。`;
|
|
91
70
|
function inside(root, path) {
|
|
92
71
|
const value = relative(root, path);
|
|
93
72
|
return value !== ".." && !value.startsWith(`..${sep}`) && !isAbsolute(value);
|
|
@@ -128,21 +107,33 @@ async function readSource(home, file) {
|
|
|
128
107
|
await handle.close();
|
|
129
108
|
}
|
|
130
109
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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;
|
|
142
123
|
}
|
|
143
|
-
|
|
144
|
-
|
|
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;
|
|
145
135
|
}
|
|
136
|
+
return text.length;
|
|
146
137
|
}
|
|
147
138
|
function normalizeLimit(requestedLimit, max, safe) {
|
|
148
139
|
if (requestedLimit <= max)
|
|
@@ -152,6 +143,50 @@ function normalizeLimit(requestedLimit, max, safe) {
|
|
|
152
143
|
warning: { code: "memory_limit_clamped", requestedLimit, appliedLimit: safe, recovered: true },
|
|
153
144
|
};
|
|
154
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
|
+
}
|
|
155
190
|
function readPage(text, source, command, warning) {
|
|
156
191
|
const metadata = {
|
|
157
192
|
...source,
|
|
@@ -197,19 +232,6 @@ function writeResult(result, io) {
|
|
|
197
232
|
io.out(output);
|
|
198
233
|
}
|
|
199
234
|
export async function runMemoryReadCommand(argv, io) {
|
|
200
|
-
if (!io.env.CREW_HOME || argv.includes("--help") || argv.includes("-h"))
|
|
201
|
-
return runMemoryReadUnlocked(argv, io);
|
|
202
|
-
const pendingOnly = argv[0] === "search-pending" || argv[0] === "read-pending";
|
|
203
|
-
try {
|
|
204
|
-
return await withMemoryAccess(io.env.CREW_HOME, () => runMemoryReadUnlocked(argv, io), pendingOnly ? { pendingOnly: true } : {});
|
|
205
|
-
}
|
|
206
|
-
catch (error) {
|
|
207
|
-
const reason = error instanceof Error && /^(?:local_)?memory_[a-z_]+$/u.test(error.message) ? error.message : "memory_migration_access_failed";
|
|
208
|
-
io.err(JSON.stringify({ error: reason, message: "记忆读取受到迁移或状态保护,不能当作没有记忆。" }));
|
|
209
|
-
return 1;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
async function runMemoryReadUnlocked(argv, io) {
|
|
213
235
|
let command;
|
|
214
236
|
try {
|
|
215
237
|
const { values, positionals } = parseArgs({
|
|
@@ -242,7 +264,6 @@ async function runMemoryReadUnlocked(argv, io) {
|
|
|
242
264
|
io.err(`memory_arguments_invalid: 参数不合法,记忆读取未完成;请按以下参数范围修正后重试,不要当作没有记忆。\n${USAGE}`);
|
|
243
265
|
return 2;
|
|
244
266
|
}
|
|
245
|
-
const fileWasExplicit = argv.some((arg) => arg === "--file" || arg.startsWith("--file="));
|
|
246
267
|
try {
|
|
247
268
|
if (!io.env.CREW_HOME)
|
|
248
269
|
throw new Error("memory_home_missing");
|
|
@@ -268,53 +289,10 @@ async function runMemoryReadUnlocked(argv, io) {
|
|
|
268
289
|
const source = await readSource(io.env.CREW_HOME, command.file);
|
|
269
290
|
if (command.ifHash !== undefined && command.ifHash !== source.sha256)
|
|
270
291
|
throw new Error("memory_file_changed");
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
const normalized = normalizeLimit(command.limit, SEARCH_LIMIT_MAX, SEARCH_LIMIT_SAFE);
|
|
274
|
-
const result = searchFullMemoryIndex(source.text, source.sha256, {
|
|
275
|
-
...command, limit: normalized.limit, history: command.command === "search-history",
|
|
276
|
-
}, FULL_INDEX_RESULT_BYTES);
|
|
277
|
-
const indexedFiles = new Set(["MEMORY.md", ...parseFullMemoryIndex(source.text).map((match) => match.file)]);
|
|
278
|
-
const preservedSourceFiles = await readControlledPreservedSourcePaths(io.env.CREW_HOME, source.text);
|
|
279
|
-
const legacyExcludedFiles = fileWasExplicit
|
|
280
|
-
? indexedFiles
|
|
281
|
-
: new Set([...indexedFiles, ...preservedSourceFiles]);
|
|
282
|
-
const legacy = await searchLegacyMemorySources({ agentDir: io.env.CREW_HOME, query: command.query,
|
|
283
|
-
limit: normalized.limit, outputBytes: FULL_INDEX_LEGACY_SCAN_BYTES,
|
|
284
|
-
...(command.cursor === undefined ? {} : { cursor: command.cursor }),
|
|
285
|
-
...(fileWasExplicit ? { files: [command.file], excludeFiles: legacyExcludedFiles } : { excludeFiles: legacyExcludedFiles }) });
|
|
286
|
-
if (result.matches.length === 0 && (command.cursor === undefined || legacy.matchCount > 0)) {
|
|
287
|
-
if (legacy.unindexedFiles.length > 0 && (legacy.matchCount > 0 || result.matchCount === 0)) {
|
|
288
|
-
writeResult({ ...legacy, ...(normalized.warning ? { warning: normalized.warning,
|
|
289
|
-
requestedLimit: normalized.warning.requestedLimit, limit: normalized.warning.appliedLimit } : {}) }, io);
|
|
290
|
-
return 0;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
writeResult({ source: "v2", legacy: false, ...result,
|
|
294
|
-
unindexedFiles: legacy.unindexedFiles,
|
|
295
|
-
unindexedFileCount: legacy.unindexedFileCount,
|
|
296
|
-
unindexedFilesTruncated: legacy.unindexedFilesTruncated,
|
|
297
|
-
complete: result.complete && legacy.matchCount === 0 && legacy.complete,
|
|
298
|
-
...(legacy.matchCount === 0 ? {} : { unindexedMatchCount: legacy.matchCount }),
|
|
299
|
-
...(legacy.matchCount === 0 ? {} : {
|
|
300
|
-
legacyHasMore: legacy.hasMore,
|
|
301
|
-
legacyNextCursor: legacy.nextCursor,
|
|
302
|
-
legacyNextAfterLine: legacy.nextAfterLine,
|
|
303
|
-
v2AfterLine: result.matches.at(-1)?.line ?? result.nextAfterLine,
|
|
304
|
-
}),
|
|
305
|
-
...(normalized.warning ? { warning: normalized.warning,
|
|
306
|
-
requestedLimit: normalized.warning.requestedLimit, limit: normalized.warning.appliedLimit } : {}) }, io);
|
|
307
|
-
return 0;
|
|
308
|
-
}
|
|
309
|
-
const result = command.command === "search" || command.command === "search-history"
|
|
310
|
-
? await (async () => {
|
|
292
|
+
const result = command.command === "search"
|
|
293
|
+
? (() => {
|
|
311
294
|
const normalized = normalizeLimit(command.limit, SEARCH_LIMIT_MAX, SEARCH_LIMIT_SAFE);
|
|
312
|
-
|
|
313
|
-
limit: normalized.limit, outputBytes: OUTPUT_BYTES - 256, afterLine: command.afterLine,
|
|
314
|
-
...(command.cursor === undefined ? {} : { cursor: command.cursor }),
|
|
315
|
-
...(fileWasExplicit ? { files: [command.file] } : {}) });
|
|
316
|
-
return { ...legacy, ...(normalized.warning ? { warning: normalized.warning,
|
|
317
|
-
requestedLimit: normalized.warning.requestedLimit, limit: normalized.warning.appliedLimit } : {}) };
|
|
295
|
+
return searchSource(source.text, source.sha256, { ...command, limit: normalized.limit }, normalized.warning);
|
|
318
296
|
})()
|
|
319
297
|
: (() => {
|
|
320
298
|
const normalized = normalizeLimit(command.limit, READ_LIMIT_MAX, READ_LIMIT_SAFE);
|
|
@@ -7,80 +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", allowPendingOnly = false) {
|
|
39
|
-
dslog("local_memory.v2_deferred", allowPendingOnly
|
|
40
|
-
? "迁移尚未完成,新增候选隔离保存到 pending"
|
|
41
|
-
: legacyFallback ? "旧格式继续原记忆流程,转换成功后再启用V2" : "记忆格式异常,保留原资料和待处理日志", {
|
|
42
|
-
execution_id: executionId,
|
|
43
|
-
agent_handle: handle,
|
|
44
|
-
reason: legacyFallback ? "legacy_compatible" : failure,
|
|
45
|
-
writer_mode: allowPendingOnly ? "pending_only" : legacyFallback ? "legacy" : "blocked",
|
|
46
|
-
...(allowPendingOnly ? { pending_reason: "legacy_requires_migration" } : {}),
|
|
16
|
+
export function reportLocalMemoryV2Deferred(executionId, handle, memoryPrune) {
|
|
17
|
+
dslog("local_memory.v2_deferred", "旧记忆尚未通过兼容检查,保留原资料和待处理日志", {
|
|
18
|
+
execution_id: executionId, agent_handle: handle, reason: "requires_migration"
|
|
47
19
|
});
|
|
48
|
-
if (memoryPrune
|
|
49
|
-
throw new Error(
|
|
50
|
-
}
|
|
20
|
+
if (memoryPrune)
|
|
21
|
+
throw new Error("local_memory_v2_requires_migration");
|
|
51
22
|
}
|
|
52
23
|
/** 只核对小型索引和其引用;不扫描全部历史任务、不迁移或生成新存储。 */
|
|
53
24
|
export async function resolveLocalMemoryV2Rollout(input) {
|
|
54
25
|
const environment = localMemoryV2Environment(input.serverUrl);
|
|
55
|
-
if (input.selfTraining)
|
|
56
|
-
return { enabled: false, deferred: false };
|
|
57
|
-
// 已提交的目标格式独立于新的迁移资格;关闭名单不能退回不兼容writer。
|
|
58
|
-
if (input.workspace.memory.includes("NOWWORK:MEMORY_INDEX:BEGIN schema=2")) {
|
|
59
|
-
try {
|
|
60
|
-
if (detectLocalMemoryStorageVersion(input.workspace.memory) !== 2)
|
|
61
|
-
throw new Error("invalid_storage");
|
|
62
|
-
parseFullMemoryIndex(input.workspace.memory);
|
|
63
|
-
const root = await realpath(input.workspace.dir);
|
|
64
|
-
const record = await readMemoryInventorySource(root, ".crew/memory/migration.v1.json", inventoryLimits.fileBytes);
|
|
65
|
-
const state = MigrationStateSchema.parse(JSON.parse(record.content));
|
|
66
|
-
assertMigrationStateIntegrity(state, state.identity);
|
|
67
|
-
if (state.phase !== "committed" || state.storageVersion !== 2
|
|
68
|
-
|| state.identity.agentId !== (input.agentId ?? input.handle)
|
|
69
|
-
|| state.identity.rootFingerprint !== createHash("sha256").update(root).digest("hex")
|
|
70
|
-
|| state.identity.ownerIdentity !== String((await lstat(root)).uid))
|
|
71
|
-
throw new Error("invalid_state");
|
|
72
|
-
return { enabled: true, deferred: false };
|
|
73
|
-
}
|
|
74
|
-
catch (error) {
|
|
75
|
-
return { enabled: false, deferred: true, failure: localMemoryRolloutFailure(error) };
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
if (environment === "production")
|
|
26
|
+
if (input.selfTraining || environment === "production")
|
|
79
27
|
return { enabled: false, deferred: false };
|
|
80
28
|
if (environment === "custom")
|
|
81
29
|
return { enabled: input.requested, deferred: false };
|
|
82
30
|
const deferred = { enabled: false, deferred: true };
|
|
83
31
|
const { workspace } = input;
|
|
32
|
+
if (Array.from(workspace.memory).length > LOCAL_MEMORY_TOTAL_CODE_POINT_CAP)
|
|
33
|
+
return deferred;
|
|
84
34
|
try {
|
|
85
35
|
const root = await realpath(workspace.dir);
|
|
86
36
|
const notesStat = await lstat(join(root, "notes"));
|
|
@@ -92,26 +42,8 @@ export async function resolveLocalMemoryV2Rollout(input) {
|
|
|
92
42
|
// 完整种子且无旧明细才可按空记忆接入;marker 不能替代这项核对。
|
|
93
43
|
const seededRole = /^## Role\n([\s\S]*?)\n\n## Core Goals\n/mu.exec(memory.content)?.[1];
|
|
94
44
|
if (seededRole !== undefined && memory.content === memorySeed(input.handle, seededRole)) {
|
|
95
|
-
|
|
96
|
-
&& (await readdir(join(root, "notes"))).length === 0)
|
|
97
|
-
return { enabled: true, deferred: false };
|
|
45
|
+
return (await readdir(join(root, "notes"))).length === 0 ? { enabled: true, deferred: false } : deferred;
|
|
98
46
|
}
|
|
99
|
-
if (!/NOWWORK:MEMORY_INDEX:/u.test(memory.content)) {
|
|
100
|
-
// 普通旧格式不是损坏V2。引用必须仍为根内可读文件,格式/容量例外保留原流程。
|
|
101
|
-
const entries = parseLocalMemoryIndex(memory.content);
|
|
102
|
-
// 已有受控ID或锚点的记录,不能因HTML标记丢失就退回旧writer。
|
|
103
|
-
if (entries.some(entry => !entry.id.startsWith("legacy:") || entry.anchor !== null))
|
|
104
|
-
return deferred;
|
|
105
|
-
let remaining = inventoryLimits.totalBytes - memory.bytes;
|
|
106
|
-
for (const file of new Set(entries.map(entry => entry.file))) {
|
|
107
|
-
if (!file.startsWith("notes/"))
|
|
108
|
-
return deferred;
|
|
109
|
-
remaining -= (await readMemoryInventorySource(root, file, Math.min(remaining, inventoryLimits.fileBytes))).bytes;
|
|
110
|
-
}
|
|
111
|
-
return { enabled: false, deferred: true, legacyFallback: true };
|
|
112
|
-
}
|
|
113
|
-
if (Array.from(workspace.memory).length > LOCAL_MEMORY_TOTAL_CODE_POINT_CAP)
|
|
114
|
-
return deferred;
|
|
115
47
|
const generated = /<!-- NOWWORK:MEMORY_INDEX:BEGIN schema=1 -->[\s\S]*?<!-- NOWWORK:MEMORY_INDEX:END -->/u.exec(memory.content)?.[0];
|
|
116
48
|
if (!generated || Array.from(generated).length > LOCAL_MEMORY_INDEX_CODE_POINT_CAP)
|
|
117
49
|
return deferred;
|
|
@@ -144,7 +76,7 @@ export async function resolveLocalMemoryV2Rollout(input) {
|
|
|
144
76
|
}
|
|
145
77
|
return { enabled: true, deferred: false };
|
|
146
78
|
}
|
|
147
|
-
catch
|
|
148
|
-
return
|
|
79
|
+
catch {
|
|
80
|
+
return deferred;
|
|
149
81
|
}
|
|
150
82
|
}
|