@nowcrew/daemon 0.6.88-beta.10 → 0.6.88-beta.11
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/local-execution-lease.js +22 -0
- package/dist/local-executor.js +18 -23
- 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 +154 -0
- package/dist/local-memory/inventory-source.js +159 -0
- package/dist/local-memory/runtime.js +21 -15
- 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 +1 -8
- package/package.json +1 -1
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { awaitWithCancellation } from "./runtime-cancellation.js";
|
|
2
|
+
const leaseTails = new Map();
|
|
3
|
+
/** 同一进程内的短操作或原生会话排队;等待取消也不会越过前一位持有者。 */
|
|
4
|
+
export async function withKeyedLease(key, operation, cancellation) {
|
|
5
|
+
const predecessor = leaseTails.get(key) ?? Promise.resolve();
|
|
6
|
+
// 解锁函数在等待 predecessor 前可用,保留取消等待时的队列顺序。
|
|
7
|
+
let unlock;
|
|
8
|
+
const current = new Promise(resolve => { unlock = resolve; });
|
|
9
|
+
const queued = predecessor.then(() => current);
|
|
10
|
+
leaseTails.set(key, queued);
|
|
11
|
+
try {
|
|
12
|
+
await awaitWithCancellation(predecessor, cancellation);
|
|
13
|
+
return await operation();
|
|
14
|
+
}
|
|
15
|
+
finally {
|
|
16
|
+
unlock();
|
|
17
|
+
void queued.then(() => {
|
|
18
|
+
if (leaseTails.get(key) === queued)
|
|
19
|
+
leaseTails.delete(key);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
package/dist/local-executor.js
CHANGED
|
@@ -17,6 +17,9 @@ import { extractFinalText, extractRunMeta, normalizeEvent, parseLine, } from "./
|
|
|
17
17
|
import { readSession, writeSession, pickResumeId, isNearBudget } from "./session.js";
|
|
18
18
|
import { NativeThreadContext, isolateMaintenanceSession } from "./native-thread-context.js";
|
|
19
19
|
import { prepareThreadDelivery } from "./thread-input-delivery.js";
|
|
20
|
+
import { resolveMemoryPruneWorkLog, workLogSha256 } from "./local-memory/work-log.js";
|
|
21
|
+
import { withKeyedLease } from "./local-execution-lease.js";
|
|
22
|
+
import { withWorkLogExecution, withWorkLogCommit, workLogExecutionKey } from "./local-memory/work-log-execution.js";
|
|
20
23
|
import { providerErrorFromExit } from "./provider-error.js";
|
|
21
24
|
import { createConsoleFormatter } from "./console-formatter.js";
|
|
22
25
|
import { withLocalExecutionFacts, } from "./local-execution-prompt.js";
|
|
@@ -159,28 +162,12 @@ async function launchLegacyRuntime(request) {
|
|
|
159
162
|
function resolvePrompt(prompt, context) {
|
|
160
163
|
return typeof prompt === "string" ? prompt : prompt(context);
|
|
161
164
|
}
|
|
162
|
-
const nativeSessionLeaseTails = new Map();
|
|
163
165
|
const activeMemoryPrunes = new Map();
|
|
164
|
-
async function withKeyedLease(key, operation, cancellation) {
|
|
165
|
-
const predecessor = nativeSessionLeaseTails.get(key) ?? Promise.resolve();
|
|
166
|
-
let release;
|
|
167
|
-
const current = new Promise((resolve) => { release = resolve; });
|
|
168
|
-
const tail = predecessor.then(() => current);
|
|
169
|
-
nativeSessionLeaseTails.set(key, tail);
|
|
170
|
-
try {
|
|
171
|
-
await awaitWithCancellation(predecessor, cancellation);
|
|
172
|
-
return await operation();
|
|
173
|
-
}
|
|
174
|
-
finally {
|
|
175
|
-
release();
|
|
176
|
-
void tail.then(() => {
|
|
177
|
-
if (nativeSessionLeaseTails.get(key) === tail)
|
|
178
|
-
nativeSessionLeaseTails.delete(key);
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
166
|
export async function executeLocal(input, callbacks = {}, dependencies = {}) {
|
|
183
167
|
input = isolateMaintenanceSession(input);
|
|
168
|
+
return withWorkLogExecution(workLogExecutionKey(input.launch.agentsRoot, input.handle, input.taskKey), () => executeLocalWithSession(input, callbacks, dependencies), dependencies.cancellation);
|
|
169
|
+
}
|
|
170
|
+
async function executeLocalWithSession(input, callbacks, dependencies) {
|
|
184
171
|
const versionedProjectSkills = input.projectSkills !== undefined
|
|
185
172
|
&& !Array.isArray(input.projectSkills);
|
|
186
173
|
if (versionedProjectSkills && (input.runtime.name !== "codex" && input.runtime.name !== "claude")) {
|
|
@@ -357,7 +344,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
357
344
|
? await rotateAgentSession(workspace.sessionDir)
|
|
358
345
|
: workspace.agentSessionId);
|
|
359
346
|
await materializePrivateCodexHome({ runtime: runtime.name, homeDir: workspace.homeDir, ...(input.launch.codexHomeMode === undefined ? {} : { mode: input.launch.codexHomeMode }), ...(sourceCodexHome === undefined ? {} : { sourceHome: sourceCodexHome }), ...(launchSessionId === undefined ? {} : { sessionId: launchSessionId }), ...(dependencies.cancellation === undefined ? {} : { cancellation: dependencies.cancellation }), ...(dependencies.materializeDefaultCodexHome === undefined ? {} : { materialize: dependencies.materializeDefaultCodexHome }) });
|
|
360
|
-
|
|
347
|
+
let promptContext = {
|
|
361
348
|
workspace: executionWorkspace,
|
|
362
349
|
resuming,
|
|
363
350
|
rotatedForBudget,
|
|
@@ -396,8 +383,13 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
396
383
|
}
|
|
397
384
|
}
|
|
398
385
|
const attachmentPlan = routeRuntimeAttachments(runtime.name, materialized?.attachments ?? []);
|
|
399
|
-
|
|
386
|
+
let baseWakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
|
|
400
387
|
memoryPruneTraceId = parseMemoryPruneTraceId(baseWakePrompt);
|
|
388
|
+
if (memoryPruneTraceId !== null) {
|
|
389
|
+
executionWorkspace = await resolveMemoryPruneWorkLog(executionWorkspace, input);
|
|
390
|
+
promptContext = { ...promptContext, workspace: executionWorkspace };
|
|
391
|
+
baseWakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
|
|
392
|
+
}
|
|
401
393
|
if (input.localMemoryV2Enabled === true && memoryPruneTraceId !== null
|
|
402
394
|
&& isLocalMemoryV2Eligible({
|
|
403
395
|
enabledForHandle: true,
|
|
@@ -937,14 +929,17 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
|
|
|
937
929
|
memoryPruneRuntimeExitCode = exitCode;
|
|
938
930
|
if (localMemoryResultPath !== null) {
|
|
939
931
|
memoryPruneFailurePhase = "memory_commit";
|
|
940
|
-
const
|
|
941
|
-
|
|
932
|
+
const resultPath = localMemoryResultPath;
|
|
933
|
+
const commit = () => commitLocalMemoryResultAfterExit({
|
|
934
|
+
resultPath,
|
|
942
935
|
agentDir: executionWorkspace.dir,
|
|
943
936
|
executionId: input.executionId,
|
|
944
937
|
pruneTraceId: memoryPruneTraceId,
|
|
945
938
|
exitCode,
|
|
946
939
|
workLogPath: executionWorkspace.workLogPath,
|
|
940
|
+
expectedWorkLogSha256: workLogSha256(executionWorkspace.workLog),
|
|
947
941
|
});
|
|
942
|
+
const committed = exitCode !== 0 ? await commit() : await withWorkLogCommit(workLogExecutionKey(input.launch.agentsRoot, input.handle, input.taskKey), commit, dependencies.cancellation);
|
|
948
943
|
if (committed.status === "failed" || committed.status === "missing") {
|
|
949
944
|
const reason = committed.error ?? "local_memory_result_missing";
|
|
950
945
|
dslog("local_memory.commit_failed", "结构化本地记忆提交或清理未完成,保留可用恢复材料", {
|
|
@@ -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
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { constants } from "node:fs";
|
|
3
|
-
import { lstat, mkdir, open, readFile,
|
|
4
|
-
import {
|
|
3
|
+
import { lstat, mkdir, open, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
5
|
import { applyLocalMemoryResult, parseLocalMemoryResult } from "./write.js";
|
|
6
|
+
import { verifyWorkLogSource } from "./work-log.js";
|
|
6
7
|
export function isLocalMemoryV2Eligible(input) {
|
|
7
8
|
// The allowlist is the rollout boundary. The marker and seed/pre-existing
|
|
8
9
|
// state are retained as diagnostics and for backward-compatible marker
|
|
@@ -80,6 +81,11 @@ export function renderLocalMemoryV2WriteGuard() {
|
|
|
80
81
|
## V2 shared memory write safety
|
|
81
82
|
本节覆盖前文关于普通任务直接更新共享记忆的例外规则。本 Agent 已启用结构化本地记忆 V2。普通任务不得直接编辑共享的 $CREW_HOME/MEMORY.md 或 $CREW_HOME/notes/ 文件;即使用户明确要求记住内容,也只把候选、用户授权和分类理由记录到本任务的 $CREW_TASK_LOG,留给任务结束后的 memory-prune 结构化收尾应用。只有 memory-prune 收尾提示明确要求写入 daemon 提供的 result 文件时,才按该协议提交候选。`;
|
|
82
83
|
}
|
|
84
|
+
function workLogFailure(error) {
|
|
85
|
+
const reason = error instanceof Error ? error.message : "";
|
|
86
|
+
return { status: "failed", error: reason === "local_memory_work_log_source_changed"
|
|
87
|
+
|| reason === "local_memory_work_log_version_missing" ? reason : "local_memory_work_log_cleanup_failed" };
|
|
88
|
+
}
|
|
83
89
|
export async function commitLocalMemoryResultAfterExit(input) {
|
|
84
90
|
if (input.exitCode !== 0)
|
|
85
91
|
return { status: "skipped" };
|
|
@@ -97,6 +103,14 @@ export async function commitLocalMemoryResultAfterExit(input) {
|
|
|
97
103
|
if (result.outcome === "conflict" || result.outcome === "failed") {
|
|
98
104
|
return { status: "failed", error: `local_memory_result_${result.outcome}` };
|
|
99
105
|
}
|
|
106
|
+
if (input.workLogPath !== undefined) {
|
|
107
|
+
try {
|
|
108
|
+
await verifyWorkLogSource(input.agentDir, input.workLogPath, input.expectedWorkLogSha256);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
return workLogFailure(error);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
100
114
|
await applyLocalMemoryResult({
|
|
101
115
|
agentDir: input.agentDir,
|
|
102
116
|
result,
|
|
@@ -109,20 +123,12 @@ export async function commitLocalMemoryResultAfterExit(input) {
|
|
|
109
123
|
}
|
|
110
124
|
if (input.workLogPath !== undefined) {
|
|
111
125
|
try {
|
|
112
|
-
const
|
|
113
|
-
if (relativeLogPath === "" || relativeLogPath === ".." || relativeLogPath.startsWith(`..${sep}`)
|
|
114
|
-
|| isAbsolute(relativeLogPath))
|
|
115
|
-
throw new Error("local_memory_work_log_path_invalid");
|
|
116
|
-
const expectedLogPath = join(await realpath(input.agentDir), relativeLogPath);
|
|
117
|
-
const before = await lstat(input.workLogPath);
|
|
118
|
-
if (!before.isFile() || before.isSymbolicLink())
|
|
119
|
-
throw new Error("local_memory_work_log_path_invalid");
|
|
120
|
-
if (await realpath(input.workLogPath) !== expectedLogPath)
|
|
121
|
-
throw new Error("local_memory_work_log_path_invalid");
|
|
126
|
+
const before = await verifyWorkLogSource(input.agentDir, input.workLogPath, input.expectedWorkLogSha256);
|
|
122
127
|
const handle = await open(input.workLogPath, constants.O_WRONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
|
123
128
|
try {
|
|
124
129
|
const opened = await handle.stat();
|
|
125
|
-
if (!opened.isFile() || opened.dev !== before.dev || opened.ino !== before.ino
|
|
130
|
+
if (!opened.isFile() || opened.dev !== before.dev || opened.ino !== before.ino
|
|
131
|
+
|| opened.size !== before.size || opened.mtimeMs !== before.mtimeMs || opened.ctimeMs !== before.ctimeMs) {
|
|
126
132
|
throw new Error("local_memory_work_log_path_changed");
|
|
127
133
|
}
|
|
128
134
|
await handle.truncate(0);
|
|
@@ -131,8 +137,8 @@ export async function commitLocalMemoryResultAfterExit(input) {
|
|
|
131
137
|
await handle.close();
|
|
132
138
|
}
|
|
133
139
|
}
|
|
134
|
-
catch {
|
|
135
|
-
return
|
|
140
|
+
catch (error) {
|
|
141
|
+
return workLogFailure(error);
|
|
136
142
|
}
|
|
137
143
|
}
|
|
138
144
|
try {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { withKeyedLease } from "../local-execution-lease.js";
|
|
3
|
+
// 只记录尚未结束的本机执行,最后一个执行结束即移除,不落盘。
|
|
4
|
+
const activeExecutions = new Map();
|
|
5
|
+
export function workLogExecutionKey(agentsRoot, handle, taskKey) {
|
|
6
|
+
return `work-log:${JSON.stringify([resolve(agentsRoot, handle), taskKey ?? "default"])}`;
|
|
7
|
+
}
|
|
8
|
+
export async function withWorkLogExecution(key, operation, cancellation) {
|
|
9
|
+
// 只在登记期间持锁;普通任务与收尾的模型调用仍可并行。
|
|
10
|
+
await withKeyedLease(key, async () => {
|
|
11
|
+
activeExecutions.set(key, (activeExecutions.get(key) ?? 0) + 1);
|
|
12
|
+
}, cancellation);
|
|
13
|
+
try {
|
|
14
|
+
return await operation();
|
|
15
|
+
}
|
|
16
|
+
finally {
|
|
17
|
+
const remaining = (activeExecutions.get(key) ?? 1) - 1;
|
|
18
|
+
if (remaining === 0)
|
|
19
|
+
activeExecutions.delete(key);
|
|
20
|
+
else
|
|
21
|
+
activeExecutions.set(key, remaining);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export async function withWorkLogCommit(key, operation, cancellation) {
|
|
25
|
+
return withKeyedLease(key, async () => {
|
|
26
|
+
// 已有同任务执行可能继续写日志,不能提交旧提案;新执行等短提交结束后再登记。
|
|
27
|
+
if (activeExecutions.get(key) !== 1)
|
|
28
|
+
throw new Error("local_memory_work_log_execution_conflict");
|
|
29
|
+
return operation();
|
|
30
|
+
}, cancellation);
|
|
31
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { lstat, readFile, realpath } from "node:fs/promises";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { safeKey } from "../workspace.js";
|
|
5
|
+
/** 收尾会话可以换目录,但仍应消费同一任务已写好的日志;不迁移文件或复用会话。 */
|
|
6
|
+
export async function resolveMemoryPruneWorkLog(workspace, input) {
|
|
7
|
+
// 旧映射有损时无法证明是同一任务,不能用它查找或清理来源。
|
|
8
|
+
if (input.keyMode !== "opaque" || !input.taskKey || safeKey(input.taskKey) !== input.taskKey) {
|
|
9
|
+
return workspace;
|
|
10
|
+
}
|
|
11
|
+
const legacyPath = join(workspace.dir, "tasks", input.taskKey, "work-log.md");
|
|
12
|
+
if (legacyPath === workspace.workLogPath)
|
|
13
|
+
return workspace;
|
|
14
|
+
let legacyStat;
|
|
15
|
+
try {
|
|
16
|
+
legacyStat = await lstat(legacyPath);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (error.code === "ENOENT")
|
|
20
|
+
return workspace;
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
if (!legacyStat.isFile() || legacyStat.isSymbolicLink()
|
|
24
|
+
|| await realpath(legacyPath) !== join(await realpath(workspace.dir), "tasks", input.taskKey, "work-log.md")) {
|
|
25
|
+
throw new Error("local_memory_work_log_path_invalid");
|
|
26
|
+
}
|
|
27
|
+
const workLog = await readFile(legacyPath, "utf8");
|
|
28
|
+
if (workLog.length === 0)
|
|
29
|
+
return workspace;
|
|
30
|
+
if ((await readFile(workspace.workLogPath, "utf8")).length > 0) {
|
|
31
|
+
throw new Error("local_memory_work_log_source_conflict");
|
|
32
|
+
}
|
|
33
|
+
return { ...workspace, workLogPath: legacyPath, workLog };
|
|
34
|
+
}
|
|
35
|
+
export function workLogSha256(content) {
|
|
36
|
+
return createHash("sha256").update(content).digest("hex");
|
|
37
|
+
}
|
|
38
|
+
/** 调用方须在同任务的提交互斥边界内校验;指纹对应实际注入模型的那一版内容。 */
|
|
39
|
+
export async function verifyWorkLogSource(agentDir, path, expectedSha256) {
|
|
40
|
+
if (expectedSha256 === undefined)
|
|
41
|
+
throw new Error("local_memory_work_log_version_missing");
|
|
42
|
+
const relativePath = relative(resolve(agentDir), resolve(path));
|
|
43
|
+
if (relativePath === "" || relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) {
|
|
44
|
+
throw new Error("local_memory_work_log_path_invalid");
|
|
45
|
+
}
|
|
46
|
+
const before = await lstat(path);
|
|
47
|
+
if (!before.isFile() || before.isSymbolicLink()
|
|
48
|
+
|| await realpath(path) !== join(await realpath(agentDir), relativePath)) {
|
|
49
|
+
throw new Error("local_memory_work_log_path_invalid");
|
|
50
|
+
}
|
|
51
|
+
const content = await readFile(path);
|
|
52
|
+
const after = await lstat(path);
|
|
53
|
+
if (workLogSha256(content) !== expectedSha256 || before.dev !== after.dev || before.ino !== after.ino
|
|
54
|
+
|| before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs) {
|
|
55
|
+
throw new Error("local_memory_work_log_source_changed");
|
|
56
|
+
}
|
|
57
|
+
return after;
|
|
58
|
+
}
|
|
@@ -4,7 +4,7 @@ import { dirname, join, relative, resolve, sep } from "node:path";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { dslog } from "../slog.js";
|
|
6
6
|
import { LocalMemoryChangeSchema, LocalMemorySingleLineText, validateLocalMemoryChange, } from "./candidate.js";
|
|
7
|
-
import { LOCAL_MEMORY_INDEX_CODE_POINT_CAP, LOCAL_MEMORY_INDEX_ENTRY_CAP, LOCAL_MEMORY_TOTAL_CODE_POINT_CAP,
|
|
7
|
+
import { LOCAL_MEMORY_INDEX_CODE_POINT_CAP, LOCAL_MEMORY_INDEX_ENTRY_CAP, LOCAL_MEMORY_TOTAL_CODE_POINT_CAP, renderGeneratedIndex, selectLocalMemoryIndexEntries, } from "./index-budget.js";
|
|
8
8
|
import { persistPendingMemoryCandidate, readPendingMemoryCandidates, removePendingMemoryCandidate, } from "./pending.js";
|
|
9
9
|
const StatusChangeSchema = z.object({
|
|
10
10
|
memoryId: LocalMemorySingleLineText,
|
|
@@ -98,13 +98,6 @@ function parseUnmarkedMemoryIndex(memory) {
|
|
|
98
98
|
const section = indexSection(memory).replace(GENERATED_INDEX_PATTERN, "");
|
|
99
99
|
return parseCards(section);
|
|
100
100
|
}
|
|
101
|
-
function renderGeneratedIndex(entries) {
|
|
102
|
-
const cards = entries
|
|
103
|
-
.filter((entry) => entry.status === "pinned" || entry.status === "active")
|
|
104
|
-
.map(renderLocalMemoryIndexCard)
|
|
105
|
-
.join("\n");
|
|
106
|
-
return `<!-- NOWWORK:MEMORY_INDEX:BEGIN schema=1 -->\n${cards}<!-- NOWWORK:MEMORY_INDEX:END -->`;
|
|
107
|
-
}
|
|
108
101
|
function replaceGeneratedIndex(memory, entries) {
|
|
109
102
|
const generated = renderGeneratedIndex(entries);
|
|
110
103
|
if (GENERATED_INDEX_PATTERN.test(memory))
|