@nowcrew/daemon 0.6.89 → 0.6.91-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -3
- package/dist/agent-ability/materializer.js +21 -10
- 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 +49 -1
- 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/daemon-updater.js +2 -12
- 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 +41 -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 -178
- package/dist/storage-identity.js +0 -3
- 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 +13 -12
- 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 -241
- 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 -447
- package/dist/storage-worker-handoff.js +0 -151
- package/dist/storage-worker-operation.js +0 -294
- package/dist/storage-worker-status.js +0 -259
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { LOCAL_MEMORY_INDEX_CODE_POINT_CAP, LOCAL_MEMORY_INDEX_ENTRY_CAP, LOCAL_MEMORY_TOTAL_CODE_POINT_CAP, renderGeneratedIndex, } from "./index-budget.js";
|
|
3
|
+
import { parseLocalMemoryIndex } from "./write.js";
|
|
4
|
+
const GENERATED = /<!--\s*NOWWORK:MEMORY_INDEX:BEGIN\s+schema=1\s*-->[\s\S]*?<!--\s*NOWWORK:MEMORY_INDEX:END\s*-->/gu;
|
|
5
|
+
const REFERENCE = /^notes\/(?!.*(?:^|\/)\.\.?\/)(?:[^/\\\u0000-\u001f]+\/)*[^/\\\u0000-\u001f]+\.md$/u;
|
|
6
|
+
const FIELD = /^-\s+([a-z][a-z_]*)\s*:\s*(.*?)\s*$/iu;
|
|
7
|
+
const WRITABLE_FILES = new Set(["preferences", "project-rules", "testing", "debugging", "git", "incidents", "lessons"]
|
|
8
|
+
.map((category) => `notes/${category}.md`));
|
|
9
|
+
const length = (text) => Array.from(text).length;
|
|
10
|
+
const digest = (text) => createHash("sha256").update(text).digest("hex");
|
|
11
|
+
export function previewMemoryInventory(sources, readComplete, mode = "migration") {
|
|
12
|
+
const issues = [];
|
|
13
|
+
const entries = [];
|
|
14
|
+
const sourceMap = new Map(sources.map((source) => [source.path, source]));
|
|
15
|
+
const memory = sourceMap.get("MEMORY.md")?.content ?? "";
|
|
16
|
+
const add = (code, path = "MEMORY.md") => issues.push({ code, path });
|
|
17
|
+
// 掩盖注释但保留UTF-16位置,避免把模板示例当成条目,也保留原文供人工复核。
|
|
18
|
+
const comments = [...memory.matchAll(/<!--[\s\S]*?-->/gu)];
|
|
19
|
+
// 现有写入器允许正文包含箭头 A --> B;未闭合的注释仍须拒绝。
|
|
20
|
+
if ((memory.match(/<!--/gu) ?? []).length !== comments.length
|
|
21
|
+
|| (mode === "migration" && (memory.match(/-->/gu) ?? []).length !== comments.length))
|
|
22
|
+
add("comment_boundary_invalid");
|
|
23
|
+
const masked = memory.replace(/<!--[\s\S]*?-->/gu, (comment) => comment.replace(/[^\r\n]/gu, (character) => " ".repeat(character.length)));
|
|
24
|
+
const sections = [...masked.matchAll(/^##[\t ]+Knowledge Index[\t ]*\r?$/gmu)];
|
|
25
|
+
const heading = sections[0];
|
|
26
|
+
if (sections.length !== 1 || heading?.index === undefined)
|
|
27
|
+
add("index_heading_invalid");
|
|
28
|
+
const start = heading === undefined ? 0 : heading.index + heading[0].length;
|
|
29
|
+
const next = /^##[\t ]+(?!#)/mu.exec(masked.slice(start));
|
|
30
|
+
const end = next?.index === undefined ? memory.length : start + next.index;
|
|
31
|
+
const generated = [...memory.matchAll(GENERATED)];
|
|
32
|
+
const markerCount = (memory.match(/NOWWORK:MEMORY_INDEX:/gu) ?? []).length;
|
|
33
|
+
if (markerCount !== generated.length * 2 || generated.length > 1
|
|
34
|
+
|| generated.some((block) => block.index < start || block.index + block[0].length > end))
|
|
35
|
+
add("index_marker_invalid");
|
|
36
|
+
const heads = [...masked.slice(start, end).matchAll(/^###[\t ]+(.+?)[\t ]*\r?$/gmu)];
|
|
37
|
+
if (heads.length > 1000)
|
|
38
|
+
add("card_limit_exceeded");
|
|
39
|
+
const ranges = [];
|
|
40
|
+
for (const [i, head] of heads.slice(0, 1000).entries()) {
|
|
41
|
+
const cardStart = start + head.index;
|
|
42
|
+
const cardEnd = heads[i + 1]?.index === undefined ? end : start + heads[i + 1].index;
|
|
43
|
+
const tailStart = cardStart + head[0].length;
|
|
44
|
+
const tail = masked.slice(tailStart, cardEnd);
|
|
45
|
+
const fields = {};
|
|
46
|
+
const issueStart = issues.length;
|
|
47
|
+
let lastFieldEnd = tailStart;
|
|
48
|
+
let proseSeen = false;
|
|
49
|
+
for (const line of tail.matchAll(/[^\n]*(?:\n|$)/gu)) {
|
|
50
|
+
const field = FIELD.exec(line[0].replace(/\r?\n$/u, ""));
|
|
51
|
+
if (field) {
|
|
52
|
+
const key = field[1].toLowerCase();
|
|
53
|
+
if (fields[key] !== undefined)
|
|
54
|
+
add("duplicate_field");
|
|
55
|
+
if (proseSeen)
|
|
56
|
+
add("ambiguous_card_body");
|
|
57
|
+
fields[key] = field[2];
|
|
58
|
+
lastFieldEnd = tailStart + line.index + line[0].length;
|
|
59
|
+
}
|
|
60
|
+
else if (line[0].trim())
|
|
61
|
+
proseSeen = true;
|
|
62
|
+
}
|
|
63
|
+
const rawCard = memory.slice(cardStart, lastFieldEnd);
|
|
64
|
+
const format = generated.some((block) => cardStart >= block.index && cardStart < block.index + block[0].length)
|
|
65
|
+
? "managed" : "legacy";
|
|
66
|
+
const parsed = parseLocalMemoryIndex(`## Knowledge Index\n${rawCard}`)[0];
|
|
67
|
+
// 状态更新会把已归一化的空触发词数组写回;字段缺失仍是格式错误。
|
|
68
|
+
const missingTriggers = mode === "existing_v2" && format === "managed" ? fields.triggers === undefined : !fields.triggers;
|
|
69
|
+
if (!parsed || !fields.type || !fields.summary || missingTriggers || !fields.priority || !fields.updated
|
|
70
|
+
|| (format === "legacy" && !fields.scope)
|
|
71
|
+
|| !["procedural", "semantic", "episodic", "preference"].includes(fields.type)
|
|
72
|
+
|| !["high", "medium", "low"].includes(fields.priority))
|
|
73
|
+
add("invalid_card");
|
|
74
|
+
const file = fields.file ?? "";
|
|
75
|
+
if (!REFERENCE.test(file) || file.split("/").some((part) => part === "." || part === ".."))
|
|
76
|
+
add("invalid_reference", file);
|
|
77
|
+
const detail = sourceMap.get(file)?.content ?? null;
|
|
78
|
+
if (detail === null)
|
|
79
|
+
add("missing_detail", file);
|
|
80
|
+
let proposed = null;
|
|
81
|
+
if (parsed) {
|
|
82
|
+
const id = format === "legacy" ? `mem_${digest(rawCard).slice(0, 32)}` : parsed.id;
|
|
83
|
+
proposed = { ...parsed, id, anchor: id, revision: format === "legacy" ? 1 : parsed.revision };
|
|
84
|
+
if (format === "managed") {
|
|
85
|
+
if (!fields.id || !/^[a-zA-Z0-9_-]+$/u.test(fields.id) || fields.anchor !== fields.id
|
|
86
|
+
|| !/^[1-9][0-9]*$/u.test(fields.revision ?? "") || !Number.isSafeInteger(parsed.revision)
|
|
87
|
+
|| !["active", "pinned"].includes(fields.status ?? ""))
|
|
88
|
+
add("managed_metadata_invalid");
|
|
89
|
+
const begin = `<!-- NOWWORK:MEMORY_ENTRY:BEGIN id=${parsed.id} revision=${parsed.revision} -->`;
|
|
90
|
+
const finish = `<!-- NOWWORK:MEMORY_ENTRY:END id=${parsed.id} -->`;
|
|
91
|
+
if (!detail?.includes(begin) || !detail.includes(finish)
|
|
92
|
+
|| detail.indexOf(finish) < detail.indexOf(begin)
|
|
93
|
+
|| detail.split(begin).length !== 2 || detail.split(finish).length !== 2)
|
|
94
|
+
add("managed_anchor_invalid", file);
|
|
95
|
+
}
|
|
96
|
+
else if (fields.status && fields.status !== "active") {
|
|
97
|
+
add("legacy_status_requires_review", file);
|
|
98
|
+
}
|
|
99
|
+
if (format === "legacy" && /NOWWORK:MEMORY_(?:ENTRY|INDEX):/u.test(detail ?? ""))
|
|
100
|
+
add("legacy_detail_contains_markers", file);
|
|
101
|
+
}
|
|
102
|
+
entries.push({
|
|
103
|
+
title: head[1], format, disposition: issues.length === issueStart ? "review_required" : "blocked",
|
|
104
|
+
sourceFields: fields, rawCard, detail, proposed,
|
|
105
|
+
});
|
|
106
|
+
ranges.push({ start: cardStart, end: lastFieldEnd });
|
|
107
|
+
}
|
|
108
|
+
for (const entry of entries) {
|
|
109
|
+
if (entries.filter((other) => other.sourceFields.file === entry.sourceFields.file).length > 1
|
|
110
|
+
&& entry.format === "legacy")
|
|
111
|
+
add("shared_legacy_detail", entry.sourceFields.file);
|
|
112
|
+
if (entries.filter((other) => other.proposed?.id === entry.proposed?.id).length > 1)
|
|
113
|
+
add("duplicate_id");
|
|
114
|
+
// 已管理条目由ID区分,现有update允许同名;迁移预演仍要求人工审核同名来源。
|
|
115
|
+
if ((mode === "migration" || entry.format === "legacy")
|
|
116
|
+
&& entries.filter((other) => other.title.trim().toLowerCase() === entry.title.trim().toLowerCase()).length > 1)
|
|
117
|
+
add("duplicate_title");
|
|
118
|
+
}
|
|
119
|
+
const compatibilityIssues = entries.filter((entry) => !WRITABLE_FILES.has(entry.sourceFields.file ?? ""))
|
|
120
|
+
.map((entry) => ({ code: "detail_category_mapping_required", path: entry.sourceFields.file ?? "" }));
|
|
121
|
+
let preview = null;
|
|
122
|
+
if (readComplete && issues.length === 0) {
|
|
123
|
+
const index = renderGeneratedIndex(entries.map(entry => entry.proposed));
|
|
124
|
+
let retained = memory;
|
|
125
|
+
for (const range of [...ranges].reverse())
|
|
126
|
+
retained = retained.slice(0, range.start) + retained.slice(range.end);
|
|
127
|
+
retained = retained.replace(GENERATED, "");
|
|
128
|
+
// 新生成块仅替换索引卡片,角色、目标、维护说明和其它段落保留。
|
|
129
|
+
const insertAt = start;
|
|
130
|
+
const projected = retained.slice(0, insertAt) + `\n${index}\n` + retained.slice(insertAt);
|
|
131
|
+
const budget = {
|
|
132
|
+
entries: entries.length, indexCodePoints: length(index), totalCodePoints: length(projected),
|
|
133
|
+
entryCap: LOCAL_MEMORY_INDEX_ENTRY_CAP, indexCodePointCap: LOCAL_MEMORY_INDEX_CODE_POINT_CAP,
|
|
134
|
+
totalCodePointCap: LOCAL_MEMORY_TOTAL_CODE_POINT_CAP,
|
|
135
|
+
fits: entries.length <= LOCAL_MEMORY_INDEX_ENTRY_CAP && length(index) <= LOCAL_MEMORY_INDEX_CODE_POINT_CAP
|
|
136
|
+
&& length(projected) <= LOCAL_MEMORY_TOTAL_CODE_POINT_CAP,
|
|
137
|
+
};
|
|
138
|
+
preview = {
|
|
139
|
+
memory: projected, budget, writerCompatible: compatibilityIssues.length === 0,
|
|
140
|
+
notes: entries.filter((entry) => entry.format === "legacy").map((entry) => ({
|
|
141
|
+
path: entry.sourceFields.file,
|
|
142
|
+
content: `<!-- NOWWORK:MEMORY_ENTRY:BEGIN id=${entry.proposed.id} revision=1 -->\n`
|
|
143
|
+
+ `## ${entry.title}\n\n旧索引范围(仍需结合正文人工确认):${entry.sourceFields.scope}\n\n`
|
|
144
|
+
+ `${entry.detail}${entry.detail.endsWith("\n") ? "" : "\n"}`
|
|
145
|
+
+ `<!-- NOWWORK:MEMORY_ENTRY:END id=${entry.proposed.id} -->\n`,
|
|
146
|
+
})),
|
|
147
|
+
};
|
|
148
|
+
if (!budget.fits)
|
|
149
|
+
add("projection_over_budget");
|
|
150
|
+
}
|
|
151
|
+
const shownDetails = new Set();
|
|
152
|
+
const compactEntries = entries.map((entry) => {
|
|
153
|
+
const file = entry.sourceFields.file ?? "";
|
|
154
|
+
const detail = shownDetails.has(file) ? null : entry.detail;
|
|
155
|
+
shownDetails.add(file);
|
|
156
|
+
return { ...entry, detail };
|
|
157
|
+
});
|
|
158
|
+
return { entries: compactEntries, issues: [...issues, ...compatibilityIssues], preview, rawMemory: memory };
|
|
159
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { lstat, open, readdir, realpath } from "node:fs/promises";
|
|
4
|
+
import { isAbsolute, join, relative, sep } from "node:path";
|
|
5
|
+
import { TextDecoder } from "node:util";
|
|
6
|
+
const MAX_FILE_BYTES = 32 * 1024 * 1024;
|
|
7
|
+
const MAX_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
8
|
+
const MAX_ITEMS = 10_000;
|
|
9
|
+
const MAX_DEPTH = 32;
|
|
10
|
+
export const inventoryLimits = {
|
|
11
|
+
fileBytes: MAX_FILE_BYTES, totalBytes: MAX_TOTAL_BYTES, items: MAX_ITEMS, depth: MAX_DEPTH,
|
|
12
|
+
};
|
|
13
|
+
function errorCode(error) {
|
|
14
|
+
if (error instanceof TypeError)
|
|
15
|
+
return "invalid_utf8";
|
|
16
|
+
if (error instanceof Error)
|
|
17
|
+
return error.code ?? error.message;
|
|
18
|
+
return "read_failed";
|
|
19
|
+
}
|
|
20
|
+
async function safePath(root, file) {
|
|
21
|
+
const parts = file.split("/");
|
|
22
|
+
if (parts.some((part) => !part || part === "." || part === ".." || part.includes("\\"))) {
|
|
23
|
+
throw new Error("unsafe_path");
|
|
24
|
+
}
|
|
25
|
+
let path = root;
|
|
26
|
+
for (const part of parts) {
|
|
27
|
+
path = join(path, part);
|
|
28
|
+
if ((await lstat(path)).isSymbolicLink())
|
|
29
|
+
throw new Error("unsafe_path");
|
|
30
|
+
}
|
|
31
|
+
const actual = await realpath(path);
|
|
32
|
+
const rel = relative(root, actual);
|
|
33
|
+
if (actual !== path || isAbsolute(rel) || rel === ".." || rel.startsWith(`..${sep}`)) {
|
|
34
|
+
throw new Error("unsafe_path");
|
|
35
|
+
}
|
|
36
|
+
return path;
|
|
37
|
+
}
|
|
38
|
+
export async function readMemoryInventorySource(root, file, budget) {
|
|
39
|
+
const path = await safePath(root, file);
|
|
40
|
+
const handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
|
41
|
+
try {
|
|
42
|
+
const before = await handle.stat();
|
|
43
|
+
if (!before.isFile())
|
|
44
|
+
throw new Error("not_regular_file");
|
|
45
|
+
const limit = Math.min(MAX_FILE_BYTES, budget);
|
|
46
|
+
if (before.size > limit)
|
|
47
|
+
throw new Error("read_limit_exceeded");
|
|
48
|
+
const chunks = [];
|
|
49
|
+
let bytes = 0;
|
|
50
|
+
for (;;) {
|
|
51
|
+
const chunk = Buffer.alloc(Math.min(64 * 1024, limit + 1 - bytes));
|
|
52
|
+
const { bytesRead } = await handle.read(chunk, 0, chunk.length, null);
|
|
53
|
+
if (bytesRead === 0)
|
|
54
|
+
break;
|
|
55
|
+
bytes += bytesRead;
|
|
56
|
+
if (bytes > limit)
|
|
57
|
+
throw new Error("read_limit_exceeded");
|
|
58
|
+
chunks.push(chunk.subarray(0, bytesRead));
|
|
59
|
+
}
|
|
60
|
+
const after = await handle.stat();
|
|
61
|
+
const current = await lstat(await safePath(root, file));
|
|
62
|
+
if (before.ino !== current.ino || before.dev !== current.dev || before.size !== after.size
|
|
63
|
+
|| before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs)
|
|
64
|
+
throw new Error("source_changed");
|
|
65
|
+
const data = Buffer.concat(chunks);
|
|
66
|
+
return {
|
|
67
|
+
path: file, bytes,
|
|
68
|
+
sha256: createHash("sha256").update(data).digest("hex"),
|
|
69
|
+
content: new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(data),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
await handle.close();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export async function scanMemorySources(agentDir) {
|
|
77
|
+
const root = await realpath(agentDir);
|
|
78
|
+
if ((await lstat(agentDir)).isSymbolicLink())
|
|
79
|
+
throw new Error("agent_root_symlink");
|
|
80
|
+
if (!(await lstat(root)).isDirectory())
|
|
81
|
+
throw new Error("agent_root_not_directory");
|
|
82
|
+
const sources = [];
|
|
83
|
+
const issues = [];
|
|
84
|
+
const directories = new Map();
|
|
85
|
+
let totalBytes = 0;
|
|
86
|
+
let items = 0;
|
|
87
|
+
const addIssue = (code, path) => issues.push({ code, path });
|
|
88
|
+
async function visit(file, optional = false, depth = 0) {
|
|
89
|
+
if (++items > MAX_ITEMS || depth > MAX_DEPTH) {
|
|
90
|
+
addIssue("scan_limit_exceeded", file);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const path = await safePath(root, file);
|
|
95
|
+
const stat = await lstat(path);
|
|
96
|
+
if (file === "MEMORY.md" && !stat.isFile())
|
|
97
|
+
throw new Error("not_regular_file");
|
|
98
|
+
if ((file === "notes" || file === ".crew/memory/pending") && !stat.isDirectory())
|
|
99
|
+
throw new Error("not_directory");
|
|
100
|
+
if (stat.isDirectory()) {
|
|
101
|
+
const names = (await readdir(path)).sort();
|
|
102
|
+
directories.set(file, names);
|
|
103
|
+
for (const name of names) {
|
|
104
|
+
if (items >= MAX_ITEMS) {
|
|
105
|
+
addIssue("scan_limit_exceeded", file);
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
await visit(`${file}/${name}`, false, depth + 1);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
const source = await readMemoryInventorySource(root, file, MAX_TOTAL_BYTES - totalBytes);
|
|
113
|
+
totalBytes += source.bytes;
|
|
114
|
+
sources.push(source);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
const code = errorCode(error);
|
|
119
|
+
if (optional && code === "ENOENT")
|
|
120
|
+
directories.set(file, []);
|
|
121
|
+
else
|
|
122
|
+
addIssue(code, file);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
await visit("MEMORY.md");
|
|
126
|
+
await visit("notes", true);
|
|
127
|
+
await visit(".crew/memory/pending", true);
|
|
128
|
+
const resultNames = (await readdir(root)).filter((name) => /^\.memory-result\.v1-.*\.json$/u.test(name)).sort();
|
|
129
|
+
for (const name of resultNames)
|
|
130
|
+
await visit(name);
|
|
131
|
+
// 第二遍比对摘要和目录清单,发现扫描期间的变化就拒绝完整性结论;这不是跨文件事务锁。
|
|
132
|
+
for (const source of sources) {
|
|
133
|
+
try {
|
|
134
|
+
if ((await readMemoryInventorySource(root, source.path, MAX_FILE_BYTES)).sha256 !== source.sha256) {
|
|
135
|
+
addIssue("source_changed", source.path);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
addIssue(errorCode(error), source.path);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
for (const [file, names] of directories) {
|
|
143
|
+
try {
|
|
144
|
+
const current = (await readdir(await safePath(root, file))).sort();
|
|
145
|
+
if (JSON.stringify(current) !== JSON.stringify(names))
|
|
146
|
+
addIssue("source_changed", file);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (errorCode(error) !== "ENOENT" || names.length > 0)
|
|
150
|
+
addIssue("source_changed", file);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const currentResults = (await readdir(root)).filter((name) => /^\.memory-result\.v1-.*\.json$/u.test(name)).sort();
|
|
154
|
+
if (JSON.stringify(currentResults) !== JSON.stringify(resultNames))
|
|
155
|
+
addIssue("source_changed", ".memory-result.v1-*.json");
|
|
156
|
+
if (await realpath(agentDir) !== root)
|
|
157
|
+
addIssue("source_changed", "agent_root");
|
|
158
|
+
return { root, sources: sources.sort((a, b) => a.path.localeCompare(b.path)), issues, totalBytes };
|
|
159
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function normalizePendingQuery(value) {
|
|
2
|
+
return value.normalize("NFKC").toLocaleLowerCase().replace(/\s+/gu, " ").trim();
|
|
3
|
+
}
|
|
4
|
+
function containsTerm(text, term) {
|
|
5
|
+
if (!term)
|
|
6
|
+
return false;
|
|
7
|
+
const startBoundary = /^[a-z0-9]/u.test(term);
|
|
8
|
+
const endBoundary = /[a-z0-9]$/u.test(term);
|
|
9
|
+
let position = text.indexOf(term);
|
|
10
|
+
while (position >= 0) {
|
|
11
|
+
const before = text[position - 1] ?? "";
|
|
12
|
+
const after = text[position + term.length] ?? "";
|
|
13
|
+
if ((!startBoundary || !/[a-z0-9_-]/u.test(before)) && (!endBoundary || !/[a-z0-9_-]/u.test(after)))
|
|
14
|
+
return true;
|
|
15
|
+
position = text.indexOf(term, position + 1);
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
export function pendingRecallScore(item, query) {
|
|
20
|
+
const normalized = normalizePendingQuery(query);
|
|
21
|
+
if (!normalized)
|
|
22
|
+
return 0;
|
|
23
|
+
const candidate = item.candidate;
|
|
24
|
+
const terms = normalized.split(/[\s,,、。!?;::()[\]{}"'`]+/u).filter(Boolean);
|
|
25
|
+
const text = normalizePendingQuery([candidate.title, candidate.summary, ...candidate.triggers, candidate.detailCategory, candidate.detailBody].join("\n"));
|
|
26
|
+
const explicit = [candidate.title, ...candidate.triggers].map(normalizePendingQuery);
|
|
27
|
+
const explicitScore = explicit.reduce((score, term) => score + (containsTerm(normalized, term) ? 10 : 0), 0);
|
|
28
|
+
return explicitScore + terms.reduce((score, term) => score + (containsTerm(text, term) ? 1 : 0), 0);
|
|
29
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { isRecallablePending, PendingIdSchema } from "./pending-schema.js";
|
|
4
|
+
import { normalizePendingQuery, pendingRecallScore } from "./pending-match.js";
|
|
5
|
+
import { pendingDirectorySnapshot, pendingSourceError, PENDING_FILE_BYTES, readPendingSource } from "./pending-source.js";
|
|
6
|
+
const PAGE_FILES = 128;
|
|
7
|
+
const PAGE_BYTES = 1_000_000;
|
|
8
|
+
const OUTPUT_BYTES = 12 * 1024;
|
|
9
|
+
const CursorSchema = z.object({
|
|
10
|
+
v: z.literal(1), queryHash: z.string().regex(/^[a-f0-9]{64}$/u), version: z.string().regex(/^[a-f0-9]{64}$/u),
|
|
11
|
+
after: z.string().min(1).max(255), hadWarnings: z.boolean(),
|
|
12
|
+
}).strict();
|
|
13
|
+
const SearchSchema = z.object({
|
|
14
|
+
agentDir: z.string().min(1), query: z.string().trim().min(1).max(256),
|
|
15
|
+
cursor: z.string().max(2048).optional(), limit: z.number().int().min(1).max(40).default(10),
|
|
16
|
+
}).strict();
|
|
17
|
+
const excerpt = (text, cap) => Array.from(text).slice(0, cap).join("");
|
|
18
|
+
export async function searchPendingMemoryPage(input) {
|
|
19
|
+
const parsed = SearchSchema.safeParse(input);
|
|
20
|
+
if (!parsed.success)
|
|
21
|
+
throw new Error("memory_pending_arguments_invalid");
|
|
22
|
+
const args = parsed.data;
|
|
23
|
+
const queryHash = createHash("sha256").update(normalizePendingQuery(args.query)).digest("hex");
|
|
24
|
+
let cursor;
|
|
25
|
+
if (args.cursor !== undefined) {
|
|
26
|
+
try {
|
|
27
|
+
cursor = CursorSchema.parse(JSON.parse(Buffer.from(args.cursor, "base64url").toString("utf8")));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
throw new Error("memory_pending_cursor_invalid");
|
|
31
|
+
}
|
|
32
|
+
if (cursor.queryHash !== queryHash)
|
|
33
|
+
throw new Error("memory_pending_cursor_invalid");
|
|
34
|
+
}
|
|
35
|
+
const snapshot = await pendingDirectorySnapshot(args.agentDir);
|
|
36
|
+
if (cursor && cursor.version !== snapshot.version)
|
|
37
|
+
throw new Error("memory_pending_source_changed");
|
|
38
|
+
const initial = cursor ? snapshot.names.indexOf(cursor.after) + 1 : 0;
|
|
39
|
+
if (cursor && initial === 0)
|
|
40
|
+
throw new Error("memory_pending_cursor_invalid");
|
|
41
|
+
const matches = [];
|
|
42
|
+
const warnings = [];
|
|
43
|
+
let position = initial;
|
|
44
|
+
let scannedFiles = 0;
|
|
45
|
+
let scannedBytes = 0;
|
|
46
|
+
// 为游标、错误与元数据预留空间;达到输出上限前停止消费来源,不跳过未输出的命中。
|
|
47
|
+
let outputBytes = 1600;
|
|
48
|
+
while (position < snapshot.names.length && scannedFiles < PAGE_FILES && scannedBytes + PENDING_FILE_BYTES <= PAGE_BYTES) {
|
|
49
|
+
const name = snapshot.names[position];
|
|
50
|
+
let source;
|
|
51
|
+
let warning;
|
|
52
|
+
try {
|
|
53
|
+
source = await readPendingSource(args.agentDir, name);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
const code = pendingSourceError(error);
|
|
57
|
+
if (code === "memory_pending_source_changed" || code === "memory_pending_not_found")
|
|
58
|
+
throw new Error("memory_pending_source_changed");
|
|
59
|
+
warning = { file: name, code };
|
|
60
|
+
}
|
|
61
|
+
const match = source && isRecallablePending(source.candidate) && pendingRecallScore(source.candidate, args.query) > 0
|
|
62
|
+
? { pendingId: source.candidate.pendingId, sha256: source.sha256, title: source.candidate.candidate.title,
|
|
63
|
+
summary: excerpt(source.candidate.candidate.summary, 180), triggers: source.candidate.candidate.triggers.slice(0, 4),
|
|
64
|
+
operation: "create", state: "capacity_deferred" }
|
|
65
|
+
: null;
|
|
66
|
+
const addition = match ?? warning;
|
|
67
|
+
const size = addition ? Buffer.byteLength(JSON.stringify(addition), "utf8") + 2 : 0;
|
|
68
|
+
if (outputBytes + size > OUTPUT_BYTES)
|
|
69
|
+
break;
|
|
70
|
+
if (match)
|
|
71
|
+
matches.push(match);
|
|
72
|
+
if (warning)
|
|
73
|
+
warnings.push(warning);
|
|
74
|
+
outputBytes += size;
|
|
75
|
+
scannedFiles += 1;
|
|
76
|
+
scannedBytes += source?.bytes ?? PENDING_FILE_BYTES;
|
|
77
|
+
position += 1;
|
|
78
|
+
if (matches.length >= args.limit)
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
if ((await pendingDirectorySnapshot(args.agentDir)).version !== snapshot.version)
|
|
82
|
+
throw new Error("memory_pending_source_changed");
|
|
83
|
+
const hasMore = position < snapshot.names.length;
|
|
84
|
+
const hadWarnings = cursor?.hadWarnings === true || warnings.length > 0;
|
|
85
|
+
const nextCursor = hasMore ? Buffer.from(JSON.stringify({ v: 1, queryHash, version: snapshot.version,
|
|
86
|
+
after: snapshot.names[position - 1], hadWarnings })).toString("base64url") : null;
|
|
87
|
+
return { matches, warnings, scannedFiles, scannedBytes, hasMore, nextCursor, complete: !hasMore && !hadWarnings };
|
|
88
|
+
}
|
|
89
|
+
export async function readPendingMemoryBody(input) {
|
|
90
|
+
if (!PendingIdSchema.safeParse(input.pendingId).success)
|
|
91
|
+
throw new Error("memory_pending_id_invalid");
|
|
92
|
+
const source = await readPendingSource(input.agentDir, `${input.pendingId}.json`);
|
|
93
|
+
if (input.ifHash !== undefined && input.ifHash !== source.sha256)
|
|
94
|
+
throw new Error("memory_pending_source_changed");
|
|
95
|
+
if (!isRecallablePending(source.candidate))
|
|
96
|
+
throw new Error("memory_pending_not_recallable");
|
|
97
|
+
const candidate = source.candidate.candidate;
|
|
98
|
+
return { pendingId: source.candidate.pendingId, sha256: source.sha256, title: candidate.title,
|
|
99
|
+
operation: "create", state: "capacity_deferred", content: candidate.detailBody };
|
|
100
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { LocalMemoryChangeSchema, validateLocalMemoryChange } from "./candidate.js";
|
|
3
|
+
// 索引会按分隔符展开触发词;16 个最多 80 UTF-16 单元的输入可产生 0..640 项。
|
|
4
|
+
const ArchivedMemoryChangeSchema = LocalMemoryChangeSchema.extend({
|
|
5
|
+
triggers: z.array(LocalMemoryChangeSchema.shape.triggers.element).max(640)
|
|
6
|
+
.refine((triggers) => triggers.reduce((length, trigger) => length + trigger.length, 0) <= 16 * 80),
|
|
7
|
+
});
|
|
8
|
+
export const PendingIdSchema = z.string().max(200).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u);
|
|
9
|
+
export const PendingMemorySchema = z.object({
|
|
10
|
+
schemaVersion: z.literal(1), pendingId: PendingIdSchema,
|
|
11
|
+
createdAt: z.string().min(1).max(100), reason: z.literal("index_over_budget"),
|
|
12
|
+
source: z.object({ executionId: z.string().min(1).max(500), pruneTraceId: z.string().min(1).max(500).nullable() }).strict(),
|
|
13
|
+
candidate: ArchivedMemoryChangeSchema,
|
|
14
|
+
}).strict();
|
|
15
|
+
export function parsePendingMemoryCandidate(value) {
|
|
16
|
+
const parsed = PendingMemorySchema.safeParse(value);
|
|
17
|
+
if (!parsed.success)
|
|
18
|
+
throw new Error("local_memory_pending_invalid");
|
|
19
|
+
validateLocalMemoryChange(parsed.data.candidate);
|
|
20
|
+
return parsed.data;
|
|
21
|
+
}
|
|
22
|
+
export function isRecallablePending(value) {
|
|
23
|
+
try {
|
|
24
|
+
return parsePendingMemoryCandidate(value).candidate.operation === "create";
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { lstat, open, opendir, realpath } from "node:fs/promises";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { TextDecoder } from "node:util";
|
|
6
|
+
import { parsePendingMemoryCandidate, PendingIdSchema } from "./pending-schema.js";
|
|
7
|
+
export const PENDING_FILE_BYTES = 128 * 1024;
|
|
8
|
+
const DIRECTORY_ENTRY_CAP = 10_000;
|
|
9
|
+
export function pendingSourceError(error) {
|
|
10
|
+
if (error instanceof Error && /^memory_pending_[a-z_]+$/u.test(error.message))
|
|
11
|
+
return error.message;
|
|
12
|
+
return "memory_pending_file_invalid";
|
|
13
|
+
}
|
|
14
|
+
export async function pendingReadDirectory(agentDir) {
|
|
15
|
+
const root = await realpath(agentDir);
|
|
16
|
+
let directory = root;
|
|
17
|
+
for (const part of [".crew", "memory", "pending"]) {
|
|
18
|
+
directory = join(directory, part);
|
|
19
|
+
let stat;
|
|
20
|
+
try {
|
|
21
|
+
stat = await lstat(directory);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error.code === "ENOENT")
|
|
25
|
+
return { root, directory: null };
|
|
26
|
+
throw new Error("memory_pending_directory_unreadable");
|
|
27
|
+
}
|
|
28
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || await realpath(directory) !== directory) {
|
|
29
|
+
throw new Error("memory_pending_path_invalid");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { root, directory };
|
|
33
|
+
}
|
|
34
|
+
export async function pendingFileNames(directory) {
|
|
35
|
+
if (directory === null)
|
|
36
|
+
return [];
|
|
37
|
+
const names = [];
|
|
38
|
+
let inspected = 0;
|
|
39
|
+
try {
|
|
40
|
+
for await (const item of await opendir(directory)) {
|
|
41
|
+
if (++inspected > DIRECTORY_ENTRY_CAP)
|
|
42
|
+
throw new Error("memory_pending_directory_over_budget");
|
|
43
|
+
if (item.name.endsWith(".json"))
|
|
44
|
+
names.push(item.name);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (error instanceof Error && error.message.startsWith("memory_pending_"))
|
|
49
|
+
throw error;
|
|
50
|
+
throw new Error("memory_pending_directory_unreadable");
|
|
51
|
+
}
|
|
52
|
+
return names.sort();
|
|
53
|
+
}
|
|
54
|
+
function version(stat) {
|
|
55
|
+
return [stat.dev, stat.ino, stat.mode, stat.size, stat.mtimeMs, stat.ctimeMs].join(":");
|
|
56
|
+
}
|
|
57
|
+
export async function pendingDirectorySnapshot(agentDir) {
|
|
58
|
+
const location = await pendingReadDirectory(agentDir);
|
|
59
|
+
const names = await pendingFileNames(location.directory);
|
|
60
|
+
const digest = createHash("sha256").update(location.root);
|
|
61
|
+
if (location.directory !== null)
|
|
62
|
+
digest.update(version(await lstat(location.directory)));
|
|
63
|
+
for (const name of names) {
|
|
64
|
+
try {
|
|
65
|
+
digest.update(JSON.stringify([name, version(await lstat(join(location.directory, name)))]));
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
throw new Error("memory_pending_source_changed");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { ...location, names, version: digest.digest("hex") };
|
|
72
|
+
}
|
|
73
|
+
export async function readPendingSource(agentDir, name, byteBudget = PENDING_FILE_BYTES) {
|
|
74
|
+
if (!name.endsWith(".json") || !PendingIdSchema.safeParse(name.slice(0, -5)).success) {
|
|
75
|
+
throw new Error("memory_pending_id_invalid");
|
|
76
|
+
}
|
|
77
|
+
const { directory } = await pendingReadDirectory(agentDir);
|
|
78
|
+
if (directory === null)
|
|
79
|
+
throw new Error("memory_pending_not_found");
|
|
80
|
+
const path = join(directory, name);
|
|
81
|
+
const before = await lstat(path).catch(() => { throw new Error("memory_pending_not_found"); });
|
|
82
|
+
if (!before.isFile() || before.isSymbolicLink())
|
|
83
|
+
throw new Error("memory_pending_path_invalid");
|
|
84
|
+
const limit = Math.min(byteBudget, PENDING_FILE_BYTES);
|
|
85
|
+
if (before.size > limit)
|
|
86
|
+
throw new Error("memory_pending_file_over_budget");
|
|
87
|
+
const handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
|
88
|
+
try {
|
|
89
|
+
const opened = await handle.stat();
|
|
90
|
+
if (!opened.isFile() || version(opened) !== version(before))
|
|
91
|
+
throw new Error("memory_pending_source_changed");
|
|
92
|
+
const chunks = [];
|
|
93
|
+
let bytes = 0;
|
|
94
|
+
for (;;) {
|
|
95
|
+
const chunk = Buffer.alloc(Math.min(16 * 1024, limit + 1 - bytes));
|
|
96
|
+
const { bytesRead } = await handle.read(chunk, 0, chunk.length, null);
|
|
97
|
+
if (bytesRead === 0)
|
|
98
|
+
break;
|
|
99
|
+
bytes += bytesRead;
|
|
100
|
+
if (bytes > limit)
|
|
101
|
+
throw new Error("memory_pending_file_over_budget");
|
|
102
|
+
chunks.push(chunk.subarray(0, bytesRead));
|
|
103
|
+
}
|
|
104
|
+
const after = await handle.stat();
|
|
105
|
+
const currentDirectory = await pendingReadDirectory(agentDir);
|
|
106
|
+
const current = await lstat(path);
|
|
107
|
+
if (currentDirectory.directory !== directory || version(after) !== version(opened) || version(current) !== version(opened)) {
|
|
108
|
+
throw new Error("memory_pending_source_changed");
|
|
109
|
+
}
|
|
110
|
+
const buffer = Buffer.concat(chunks);
|
|
111
|
+
let parsed;
|
|
112
|
+
try {
|
|
113
|
+
const raw = new TextDecoder("utf-8", { fatal: true }).decode(buffer);
|
|
114
|
+
parsed = parsePendingMemoryCandidate(JSON.parse(raw));
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
throw new Error("memory_pending_file_invalid");
|
|
118
|
+
}
|
|
119
|
+
if (`${parsed.pendingId}.json` !== name)
|
|
120
|
+
throw new Error("memory_pending_id_invalid");
|
|
121
|
+
return { candidate: parsed, bytes, sha256: createHash("sha256").update(buffer).digest("hex") };
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
await handle.close();
|
|
125
|
+
}
|
|
126
|
+
}
|