@nowcrew/daemon 0.6.88-beta.2 → 0.6.88-beta.21

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.
Files changed (81) hide show
  1. package/README.md +43 -13
  2. package/dist/agent-memory/policy.js +3 -1
  3. package/dist/computer-profile.js +9 -4
  4. package/dist/config.js +0 -9
  5. package/dist/daemon-update-eligibility.js +1 -1
  6. package/dist/execution-journal.js +9 -6
  7. package/dist/execution-protocol.js +2 -1
  8. package/dist/execution-runner.js +5 -4
  9. package/dist/list-models.js +2 -0
  10. package/dist/local-execution-lease.js +22 -0
  11. package/dist/local-executor.js +58 -45
  12. package/dist/local-memory/candidate.js +43 -0
  13. package/dist/local-memory/index-budget.js +8 -0
  14. package/dist/local-memory/inventory-cli.js +61 -0
  15. package/dist/local-memory/inventory-preview.js +154 -0
  16. package/dist/local-memory/inventory-source.js +159 -0
  17. package/dist/local-memory/pending-match.js +29 -0
  18. package/dist/local-memory/pending-query.js +100 -0
  19. package/dist/local-memory/pending-schema.js +29 -0
  20. package/dist/local-memory/pending-source.js +126 -0
  21. package/dist/local-memory/pending.js +34 -84
  22. package/dist/local-memory/prompt.js +14 -11
  23. package/dist/local-memory/read-cli.js +97 -12
  24. package/dist/local-memory/runtime.js +24 -17
  25. package/dist/local-memory/work-log-execution.js +31 -0
  26. package/dist/local-memory/work-log.js +58 -0
  27. package/dist/local-memory/write.js +27 -58
  28. package/dist/machine-info.js +54 -17
  29. package/dist/main.js +4 -69
  30. package/dist/native-thread-context.js +59 -0
  31. package/dist/pre-admission-rejection.js +64 -0
  32. package/dist/project-skills/execution-adapter.js +3 -2
  33. package/dist/project-skills/runtime-launch.js +14 -0
  34. package/dist/prompt.js +15 -15
  35. package/dist/provider-env.js +3 -6
  36. package/dist/runner.js +14 -16
  37. package/dist/runtime-probe.js +34 -5
  38. package/dist/runtimes/acp-probe-transport.js +69 -0
  39. package/dist/runtimes/claude-session-start-hook.js +24 -0
  40. package/dist/runtimes/claude-thread-runner.js +266 -0
  41. package/dist/runtimes/claude.js +23 -4
  42. package/dist/runtimes/codex-app-server-runner.js +43 -4
  43. package/dist/runtimes/deepseek-harness-profile.js +92 -0
  44. package/dist/runtimes/deepseek-harness-runner.js +224 -0
  45. package/dist/runtimes/deepseek-harness.js +64 -11
  46. package/dist/runtimes/harness-probe-retry.js +42 -0
  47. package/dist/runtimes/kimi-acp-runner.js +63 -10
  48. package/dist/runtimes/opencode-runner.js +7 -1
  49. package/dist/serve.js +59 -101
  50. package/dist/session.js +4 -0
  51. package/dist/storage-identity.js +0 -3
  52. package/dist/storage-serve-adapter.js +6 -72
  53. package/dist/supervised-runtime.js +28 -18
  54. package/dist/thread-input-delivery.js +217 -0
  55. package/package.json +2 -3
  56. package/dist/codex-home-launch.js +0 -7
  57. package/dist/codex-home-mode-controller.js +0 -68
  58. package/dist/daemon-migration-controller.js +0 -211
  59. package/dist/daemon-migration-wiring.js +0 -22
  60. package/dist/runtimes/codex-home-migration-cli.js +0 -26
  61. package/dist/runtimes/codex-home-migration.js +0 -112
  62. package/dist/runtimes/codex-home.js +0 -236
  63. package/dist/storage-control.js +0 -212
  64. package/dist/storage-copy.js +0 -628
  65. package/dist/storage-credential-guard.js +0 -21
  66. package/dist/storage-doctor.js +0 -228
  67. package/dist/storage-manifest.js +0 -1027
  68. package/dist/storage-migration-controller.js +0 -440
  69. package/dist/storage-migration-manifest.js +0 -105
  70. package/dist/storage-migration-policy.js +0 -100
  71. package/dist/storage-runtime-controller.js +0 -381
  72. package/dist/storage-runtime-readiness.js +0 -240
  73. package/dist/storage-safe-fs.js +0 -302
  74. package/dist/storage-status-delivery.js +0 -224
  75. package/dist/storage-status-replay.js +0 -140
  76. package/dist/storage-target-confirmation.js +0 -129
  77. package/dist/storage-worker-command.js +0 -37
  78. package/dist/storage-worker-executor.js +0 -449
  79. package/dist/storage-worker-handoff.js +0 -151
  80. package/dist/storage-worker-operation.js +0 -294
  81. package/dist/storage-worker-status.js +0 -259
@@ -0,0 +1,43 @@
1
+ import { z } from "zod";
2
+ import { containsLikelySecret } from "../agent-memory/policy.js";
3
+ const CategorySchema = z.enum([
4
+ "preferences", "project-rules", "testing", "debugging", "git", "incidents", "lessons",
5
+ ]);
6
+ export const LocalMemorySingleLineText = z.string().trim().min(1).max(500).regex(/^[^\r\n]+$/u);
7
+ export const LocalMemoryChangeSchema = z.object({
8
+ operation: z.enum(["create", "update", "supersede"]),
9
+ memoryId: LocalMemorySingleLineText.regex(/^[^<>]+$/u).nullable(),
10
+ baseRevision: z.number().int().nonnegative().nullable(),
11
+ supersedesMemoryId: LocalMemorySingleLineText.regex(/^[^<>]+$/u).nullable(),
12
+ title: LocalMemorySingleLineText.max(120),
13
+ kind: z.enum(["procedural", "semantic", "episodic", "preference"]),
14
+ summary: LocalMemorySingleLineText,
15
+ triggers: z.array(LocalMemorySingleLineText.max(80)).min(1).max(16),
16
+ priority: z.enum(["high", "medium", "low"]),
17
+ detailCategory: CategorySchema,
18
+ detailBody: z.string().trim().min(1).max(12_000),
19
+ reviewAfter: LocalMemorySingleLineText.nullable(),
20
+ }).strict();
21
+ const CONTROL_CHARACTER_PATTERN = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u;
22
+ const RESERVED_MEMORY_MARKER = /NOWWORK:(?:MEMORY_ENTRY|MEMORY_INDEX):/iu;
23
+ export function validateLocalMemoryChange(change) {
24
+ const text = [change.title, change.summary, ...change.triggers, change.detailBody].join("\n");
25
+ if (containsLikelySecret(text))
26
+ throw new Error("local_memory_secret_candidate");
27
+ if (CONTROL_CHARACTER_PATTERN.test(text))
28
+ throw new Error("local_memory_control_character");
29
+ if (RESERVED_MEMORY_MARKER.test(text))
30
+ throw new Error("local_memory_reserved_marker");
31
+ if (change.operation === "create"
32
+ && (change.memoryId !== null || change.baseRevision !== null || change.supersedesMemoryId !== null)) {
33
+ throw new Error("local_memory_create_revision_invalid");
34
+ }
35
+ if (change.operation === "update"
36
+ && (change.memoryId === null || change.baseRevision === null || change.supersedesMemoryId !== null)) {
37
+ throw new Error("local_memory_update_revision_missing");
38
+ }
39
+ if (change.operation === "supersede"
40
+ && (change.memoryId !== null || change.baseRevision === null || change.supersedesMemoryId === null)) {
41
+ throw new Error("local_memory_supersede_revision_missing");
42
+ }
43
+ }
@@ -19,6 +19,14 @@ export function renderLocalMemoryIndexCard(entry) {
19
19
  + `- updated: ${entry.updated}\n`
20
20
  + (entry.reviewAfter === null ? "" : `- review_after: ${entry.reviewAfter}\n`);
21
21
  }
22
+ /** 写入和预演共用完整索引,预算包含起止标记与分隔换行。 */
23
+ export function renderGeneratedIndex(entries) {
24
+ const cards = entries
25
+ .filter(entry => PROJECTABLE_STATUSES.has(entry.status))
26
+ .map(renderLocalMemoryIndexCard)
27
+ .join("\n");
28
+ return `<!-- NOWWORK:MEMORY_INDEX:BEGIN schema=1 -->\n${cards}<!-- NOWWORK:MEMORY_INDEX:END -->`;
29
+ }
22
30
  function compareEntries(left, right) {
23
31
  const statusRank = (status) => status === "pinned" ? 0 : 1;
24
32
  const priorityRank = (priority) => (priority === "high" ? 0 : priority === "medium" ? 1 : 2);
@@ -0,0 +1,61 @@
1
+ const CommandSchema = z.object({
2
+ "agent-dir": z.string().min(1).refine(isAbsolute),
3
+ "dry-run": z.literal(true),
4
+ }).strict();
5
+ const USAGE = "只读记忆盘点与转换预演,不写文件,不支持 --apply。\n"
6
+ + "pnpm --filter @nowcrew/daemon exec tsx src/local-memory/inventory-cli.ts --agent-dir <单个Agent绝对路径> --dry-run\n"
7
+ + "报告包含原始记忆,需作为敏感本地资料保存;退出码0仅代表盘点及格式预演无阻塞,不代表批准迁移。\n";
8
+ export async function runMemoryInventoryCommand(args, io) {
9
+ if (args.length === 1 && args[0] === "--help") {
10
+ io.out(USAGE);
11
+ return 0;
12
+ }
13
+ let command;
14
+ try {
15
+ const parsed = parseArgs({ args, allowPositionals: false, options: {
16
+ "agent-dir": { type: "string" }, "dry-run": { type: "boolean" },
17
+ } });
18
+ command = CommandSchema.parse(parsed.values);
19
+ }
20
+ catch {
21
+ io.err(USAGE);
22
+ return 2;
23
+ }
24
+ try {
25
+ const scan = await scanMemorySources(command["agent-dir"]);
26
+ const scanComplete = scan.issues.length === 0;
27
+ const analysis = previewMemoryInventory(scan.sources, scanComplete);
28
+ const complete = scanComplete && !analysis.issues.some((issue) => issue.code === "missing_detail");
29
+ const issues = [...scan.issues, ...analysis.issues];
30
+ const referenced = new Set(analysis.entries.map((entry) => entry.sourceFields.file));
31
+ const files = scan.sources.map(({ path, bytes, sha256 }) => ({ path, bytes, sha256 }));
32
+ const report = {
33
+ schemaVersion: 1, mode: "dry-run", agentDir: scan.root, applySupported: false,
34
+ complete, scanComplete, requiresHumanReview: true,
35
+ coverage: ["MEMORY.md", "notes/**", ".crew/memory/pending/**", "根目录的 .memory-result.v1-*.json"],
36
+ limitations: ["不扫描历史执行目录;result清单仅覆盖Agent根目录。", "两遍摘要比对不等同跨文件原子快照,正式迁移仍需停止写入并重新核对。"],
37
+ limits: inventoryLimits, totalBytes: scan.totalBytes, files, ...analysis, issues,
38
+ pending: files.filter((file) => file.path.startsWith(".crew/memory/pending/")).map((file) => file.path),
39
+ orphanNotes: files.filter((file) => file.path.startsWith("notes/") && !referenced.has(file.path)).map((file) => file.path),
40
+ resultFiles: files.filter((file) => file.path.startsWith(".memory-result.v1-")).map((file) => file.path),
41
+ };
42
+ io.out(JSON.stringify(report, null, 2) + "\n");
43
+ return issues.length ? 1 : 0;
44
+ }
45
+ catch (error) {
46
+ const code = error instanceof Error ? error.code ?? error.message : "inventory_failed";
47
+ io.err(`记忆盘点失败:${code}\n`);
48
+ return 1;
49
+ }
50
+ }
51
+ if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
52
+ process.exitCode = await runMemoryInventoryCommand(process.argv.slice(2), {
53
+ out: (text) => process.stdout.write(text), err: (text) => process.stderr.write(text),
54
+ });
55
+ }
56
+ import { isAbsolute, resolve } from "node:path";
57
+ import { pathToFileURL } from "node:url";
58
+ import { parseArgs } from "node:util";
59
+ import { z } from "zod";
60
+ import { inventoryLimits, scanMemorySources } from "./inventory-source.js";
61
+ import { previewMemoryInventory } from "./inventory-preview.js";
@@ -0,0 +1,154 @@
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) {
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
+ if ((memory.match(/<!--/gu) ?? []).length !== comments.length
20
+ || (memory.match(/-->/gu) ?? []).length !== comments.length)
21
+ add("comment_boundary_invalid");
22
+ const masked = memory.replace(/<!--[\s\S]*?-->/gu, (comment) => comment.replace(/[^\r\n]/gu, (character) => " ".repeat(character.length)));
23
+ const sections = [...masked.matchAll(/^##[\t ]+Knowledge Index[\t ]*\r?$/gmu)];
24
+ const heading = sections[0];
25
+ if (sections.length !== 1 || heading?.index === undefined)
26
+ add("index_heading_invalid");
27
+ const start = heading === undefined ? 0 : heading.index + heading[0].length;
28
+ const next = /^##[\t ]+(?!#)/mu.exec(masked.slice(start));
29
+ const end = next?.index === undefined ? memory.length : start + next.index;
30
+ const generated = [...memory.matchAll(GENERATED)];
31
+ const markerCount = (memory.match(/NOWWORK:MEMORY_INDEX:/gu) ?? []).length;
32
+ if (markerCount !== generated.length * 2 || generated.length > 1
33
+ || generated.some((block) => block.index < start || block.index + block[0].length > end))
34
+ add("index_marker_invalid");
35
+ const heads = [...masked.slice(start, end).matchAll(/^###[\t ]+(.+?)[\t ]*\r?$/gmu)];
36
+ if (heads.length > 1000)
37
+ add("card_limit_exceeded");
38
+ const ranges = [];
39
+ for (const [i, head] of heads.slice(0, 1000).entries()) {
40
+ const cardStart = start + head.index;
41
+ const cardEnd = heads[i + 1]?.index === undefined ? end : start + heads[i + 1].index;
42
+ const tailStart = cardStart + head[0].length;
43
+ const tail = masked.slice(tailStart, cardEnd);
44
+ const fields = {};
45
+ const issueStart = issues.length;
46
+ let lastFieldEnd = tailStart;
47
+ let proseSeen = false;
48
+ for (const line of tail.matchAll(/[^\n]*(?:\n|$)/gu)) {
49
+ const field = FIELD.exec(line[0].replace(/\r?\n$/u, ""));
50
+ if (field) {
51
+ const key = field[1].toLowerCase();
52
+ if (fields[key] !== undefined)
53
+ add("duplicate_field");
54
+ if (proseSeen)
55
+ add("ambiguous_card_body");
56
+ fields[key] = field[2];
57
+ lastFieldEnd = tailStart + line.index + line[0].length;
58
+ }
59
+ else if (line[0].trim())
60
+ proseSeen = true;
61
+ }
62
+ const rawCard = memory.slice(cardStart, lastFieldEnd);
63
+ const format = generated.some((block) => cardStart >= block.index && cardStart < block.index + block[0].length)
64
+ ? "managed" : "legacy";
65
+ const parsed = parseLocalMemoryIndex(`## Knowledge Index\n${rawCard}`)[0];
66
+ if (!parsed || !fields.type || !fields.summary || !fields.triggers || !fields.priority || !fields.updated
67
+ || (format === "legacy" && !fields.scope)
68
+ || !["procedural", "semantic", "episodic", "preference"].includes(fields.type)
69
+ || !["high", "medium", "low"].includes(fields.priority))
70
+ add("invalid_card");
71
+ const file = fields.file ?? "";
72
+ if (!REFERENCE.test(file) || file.split("/").some((part) => part === "." || part === ".."))
73
+ add("invalid_reference", file);
74
+ const detail = sourceMap.get(file)?.content ?? null;
75
+ if (detail === null)
76
+ add("missing_detail", file);
77
+ let proposed = null;
78
+ if (parsed) {
79
+ const id = format === "legacy" ? `mem_${digest(rawCard).slice(0, 32)}` : parsed.id;
80
+ proposed = { ...parsed, id, anchor: id, revision: format === "legacy" ? 1 : parsed.revision };
81
+ if (format === "managed") {
82
+ if (!fields.id || !/^[a-zA-Z0-9_-]+$/u.test(fields.id) || fields.anchor !== fields.id
83
+ || !/^[1-9][0-9]*$/u.test(fields.revision ?? "") || !Number.isSafeInteger(parsed.revision)
84
+ || !["active", "pinned"].includes(fields.status ?? ""))
85
+ add("managed_metadata_invalid");
86
+ const begin = `<!-- NOWWORK:MEMORY_ENTRY:BEGIN id=${parsed.id} revision=${parsed.revision} -->`;
87
+ const finish = `<!-- NOWWORK:MEMORY_ENTRY:END id=${parsed.id} -->`;
88
+ if (!detail?.includes(begin) || !detail.includes(finish)
89
+ || detail.indexOf(finish) < detail.indexOf(begin)
90
+ || detail.split(begin).length !== 2 || detail.split(finish).length !== 2)
91
+ add("managed_anchor_invalid", file);
92
+ }
93
+ else if (fields.status && fields.status !== "active") {
94
+ add("legacy_status_requires_review", file);
95
+ }
96
+ if (format === "legacy" && /NOWWORK:MEMORY_(?:ENTRY|INDEX):/u.test(detail ?? ""))
97
+ add("legacy_detail_contains_markers", file);
98
+ }
99
+ entries.push({
100
+ title: head[1], format, disposition: issues.length === issueStart ? "review_required" : "blocked",
101
+ sourceFields: fields, rawCard, detail, proposed,
102
+ });
103
+ ranges.push({ start: cardStart, end: lastFieldEnd });
104
+ }
105
+ for (const entry of entries) {
106
+ if (entries.filter((other) => other.sourceFields.file === entry.sourceFields.file).length > 1
107
+ && entry.format === "legacy")
108
+ add("shared_legacy_detail", entry.sourceFields.file);
109
+ if (entries.filter((other) => other.proposed?.id === entry.proposed?.id).length > 1)
110
+ add("duplicate_id");
111
+ if (entries.filter((other) => other.title.trim().toLowerCase() === entry.title.trim().toLowerCase()).length > 1)
112
+ add("duplicate_title");
113
+ }
114
+ const compatibilityIssues = entries.filter((entry) => !WRITABLE_FILES.has(entry.sourceFields.file ?? ""))
115
+ .map((entry) => ({ code: "detail_category_mapping_required", path: entry.sourceFields.file ?? "" }));
116
+ let preview = null;
117
+ if (readComplete && issues.length === 0) {
118
+ const index = renderGeneratedIndex(entries.map(entry => entry.proposed));
119
+ let retained = memory;
120
+ for (const range of [...ranges].reverse())
121
+ retained = retained.slice(0, range.start) + retained.slice(range.end);
122
+ retained = retained.replace(GENERATED, "");
123
+ // 新生成块仅替换索引卡片,角色、目标、维护说明和其它段落保留。
124
+ const insertAt = start;
125
+ const projected = retained.slice(0, insertAt) + `\n${index}\n` + retained.slice(insertAt);
126
+ const budget = {
127
+ entries: entries.length, indexCodePoints: length(index), totalCodePoints: length(projected),
128
+ entryCap: LOCAL_MEMORY_INDEX_ENTRY_CAP, indexCodePointCap: LOCAL_MEMORY_INDEX_CODE_POINT_CAP,
129
+ totalCodePointCap: LOCAL_MEMORY_TOTAL_CODE_POINT_CAP,
130
+ fits: entries.length <= LOCAL_MEMORY_INDEX_ENTRY_CAP && length(index) <= LOCAL_MEMORY_INDEX_CODE_POINT_CAP
131
+ && length(projected) <= LOCAL_MEMORY_TOTAL_CODE_POINT_CAP,
132
+ };
133
+ preview = {
134
+ memory: projected, budget, writerCompatible: compatibilityIssues.length === 0,
135
+ notes: entries.filter((entry) => entry.format === "legacy").map((entry) => ({
136
+ path: entry.sourceFields.file,
137
+ content: `<!-- NOWWORK:MEMORY_ENTRY:BEGIN id=${entry.proposed.id} revision=1 -->\n`
138
+ + `## ${entry.title}\n\n旧索引范围(仍需结合正文人工确认):${entry.sourceFields.scope}\n\n`
139
+ + `${entry.detail}${entry.detail.endsWith("\n") ? "" : "\n"}`
140
+ + `<!-- NOWWORK:MEMORY_ENTRY:END id=${entry.proposed.id} -->\n`,
141
+ })),
142
+ };
143
+ if (!budget.fits)
144
+ add("projection_over_budget");
145
+ }
146
+ const shownDetails = new Set();
147
+ const compactEntries = entries.map((entry) => {
148
+ const file = entry.sourceFields.file ?? "";
149
+ const detail = shownDetails.has(file) ? null : entry.detail;
150
+ shownDetails.add(file);
151
+ return { ...entry, detail };
152
+ });
153
+ return { entries: compactEntries, issues: [...issues, ...compatibilityIssues], preview, rawMemory: memory };
154
+ }
@@ -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
+ async function readSource(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 readSource(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 readSource(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
+ }