@nowcrew/daemon 0.6.91-beta.7 → 0.6.91-beta.9

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 (39) hide show
  1. package/dist/computer-main.js +0 -0
  2. package/dist/console.js +5 -4
  3. package/dist/execution-journal.js +3 -1
  4. package/dist/execution-protocol.js +1 -1
  5. package/dist/execution-runner.js +3 -1
  6. package/dist/i18n.js +4 -3
  7. package/dist/local-execution-prompt.js +8 -3
  8. package/dist/local-executor.js +14 -35
  9. package/dist/local-memory/execution-workspace.js +36 -0
  10. package/dist/local-memory/full-index-search.js +47 -0
  11. package/dist/local-memory/index-budget.js +9 -3
  12. package/dist/local-memory/migration-access.js +118 -0
  13. package/dist/local-memory/migration-cli.js +54 -0
  14. package/dist/local-memory/migration-io.js +199 -0
  15. package/dist/local-memory/migration-plan.js +278 -0
  16. package/dist/local-memory/migration-schema.js +55 -0
  17. package/dist/local-memory/migration-store.js +424 -0
  18. package/dist/local-memory/migration-tool-entry.js +21 -0
  19. package/dist/local-memory/prompt.js +20 -4
  20. package/dist/local-memory/read-cli.js +36 -1
  21. package/dist/local-memory/rollout.js +53 -8
  22. package/dist/local-memory/runtime.js +5 -2
  23. package/dist/local-memory/storage-format.js +152 -0
  24. package/dist/local-memory/write.js +108 -17
  25. package/dist/machine-info.js +3 -0
  26. package/dist/main.js +0 -0
  27. package/dist/memory-migration-controller.js +84 -0
  28. package/dist/memory-migration-inspect.js +74 -0
  29. package/dist/memory-migration-journal.js +14 -0
  30. package/dist/memory-migration-protocol.js +14 -0
  31. package/dist/memory-migration-runtime.js +305 -0
  32. package/dist/message-file-preview.js +11 -1
  33. package/dist/prompt.js +5 -1
  34. package/dist/runner.js +1 -1
  35. package/dist/runtime-startup-gate.js +1 -1
  36. package/dist/serve.js +36 -3
  37. package/dist/token.js +1 -0
  38. package/dist/workspace-control-handler.js +16 -3
  39. package/package.json +8 -9
@@ -7,30 +7,57 @@ import { inventoryLimits, readMemoryInventorySource } from "./inventory-source.j
7
7
  import { previewMemoryInventory } from "./inventory-preview.js";
8
8
  import { parseLocalMemoryIndex } from "./write.js";
9
9
  import { dslog } from "../slog.js";
10
+ import { createHash } from "node:crypto";
11
+ import { detectLocalMemoryStorageVersion, parseFullMemoryIndex } from "./storage-format.js";
12
+ import { MigrationStateSchema } from "./migration-schema.js";
10
13
  export const LOCAL_MEMORY_V2_DEFERRED_GUIDANCE = `
11
14
 
12
15
  ## 本轮本地记忆兼容状态
13
16
  开发环境已开放 V2,但该 Agent 的旧记忆尚未通过格式兼容检查,本轮未启用结构化 V2。
14
17
  本节覆盖前文和旧会话中直接改写共享记忆的要求:本轮 MEMORY.md 与 notes 只读,不得改写、迁移或清空。
15
18
  用户要求新增或更新长期规则时,内容和授权均只记录到本任务的 $CREW_TASK_LOG;保留日志,不要清空,不要宣称长期记忆已生效。旧资料兼容处理完成前,记忆收尾会明确报告需要处理。`;
16
- export function reportLocalMemoryV2Deferred(executionId, handle, memoryPrune) {
17
- dslog("local_memory.v2_deferred", "旧记忆尚未通过兼容检查,保留原资料和待处理日志", {
18
- execution_id: executionId, agent_handle: handle, reason: "requires_migration"
19
+ export const LOCAL_MEMORY_LEGACY_GUIDANCE = `
20
+
21
+ ## 本轮本地记忆兼容状态
22
+ 本 Agent 尚未完成 V2 格式转换,本轮继续原记忆流程。用户明确要求新增或修改长期规则时,可按原规则最小范围更新共享记忆;记忆收尾继续依据任务日志更新原 MEMORY.md 和 notes,确认成功后再清理日志。
23
+ 本节覆盖旧会话中因等待 V2 转换而冻结共享记忆写入的要求,但不是新的用户写入授权;本任务如明确要求不更新记忆,继续遵守。
24
+ 保留原身份、目标及无关资料,不自行迁移或重置文件,不宣称已启用 V2。`;
25
+ export function reportLocalMemoryV2Deferred(executionId, handle, memoryPrune, legacyFallback = false) {
26
+ dslog("local_memory.v2_deferred", legacyFallback ? "旧格式继续原记忆流程,转换成功后再启用V2" : "记忆格式异常,保留原资料和待处理日志", {
27
+ execution_id: executionId, agent_handle: handle, reason: legacyFallback ? "legacy_compatible" : "requires_migration"
19
28
  });
20
- if (memoryPrune)
29
+ if (memoryPrune && !legacyFallback)
21
30
  throw new Error("local_memory_v2_requires_migration");
22
31
  }
23
32
  /** 只核对小型索引和其引用;不扫描全部历史任务、不迁移或生成新存储。 */
24
33
  export async function resolveLocalMemoryV2Rollout(input) {
25
34
  const environment = localMemoryV2Environment(input.serverUrl);
26
- if (input.selfTraining || environment === "production")
35
+ if (input.selfTraining)
36
+ return { enabled: false, deferred: false };
37
+ // 已提交的目标格式独立于新的迁移资格;关闭名单不能退回不兼容writer。
38
+ if (input.workspace.memory.includes("NOWWORK:MEMORY_INDEX:BEGIN schema=2")) {
39
+ try {
40
+ if (detectLocalMemoryStorageVersion(input.workspace.memory) !== 2)
41
+ throw new Error("invalid_storage");
42
+ parseFullMemoryIndex(input.workspace.memory);
43
+ const root = await realpath(input.workspace.dir);
44
+ const record = await readMemoryInventorySource(root, ".crew/memory/migration.v1.json", inventoryLimits.fileBytes);
45
+ const state = MigrationStateSchema.parse(JSON.parse(record.content));
46
+ if (state.phase !== "committed" || state.storageVersion !== 2 || state.identity.agentId !== input.handle
47
+ || state.identity.rootFingerprint !== createHash("sha256").update(root).digest("hex"))
48
+ throw new Error("invalid_state");
49
+ return { enabled: true, deferred: false };
50
+ }
51
+ catch {
52
+ return { enabled: false, deferred: true };
53
+ }
54
+ }
55
+ if (environment === "production")
27
56
  return { enabled: false, deferred: false };
28
57
  if (environment === "custom")
29
58
  return { enabled: input.requested, deferred: false };
30
59
  const deferred = { enabled: false, deferred: true };
31
60
  const { workspace } = input;
32
- if (Array.from(workspace.memory).length > LOCAL_MEMORY_TOTAL_CODE_POINT_CAP)
33
- return deferred;
34
61
  try {
35
62
  const root = await realpath(workspace.dir);
36
63
  const notesStat = await lstat(join(root, "notes"));
@@ -42,8 +69,26 @@ export async function resolveLocalMemoryV2Rollout(input) {
42
69
  // 完整种子且无旧明细才可按空记忆接入;marker 不能替代这项核对。
43
70
  const seededRole = /^## Role\n([\s\S]*?)\n\n## Core Goals\n/mu.exec(memory.content)?.[1];
44
71
  if (seededRole !== undefined && memory.content === memorySeed(input.handle, seededRole)) {
45
- return (await readdir(join(root, "notes"))).length === 0 ? { enabled: true, deferred: false } : deferred;
72
+ if (Array.from(memory.content).length <= LOCAL_MEMORY_TOTAL_CODE_POINT_CAP
73
+ && (await readdir(join(root, "notes"))).length === 0)
74
+ return { enabled: true, deferred: false };
46
75
  }
76
+ if (!/NOWWORK:MEMORY_INDEX:/u.test(memory.content)) {
77
+ // 普通旧格式不是损坏V2。引用必须仍为根内可读文件,格式/容量例外保留原流程。
78
+ const entries = parseLocalMemoryIndex(memory.content);
79
+ // 已有受控ID或锚点的记录,不能因HTML标记丢失就退回旧writer。
80
+ if (entries.some(entry => !entry.id.startsWith("legacy:") || entry.anchor !== null))
81
+ return deferred;
82
+ let remaining = inventoryLimits.totalBytes - memory.bytes;
83
+ for (const file of new Set(entries.map(entry => entry.file))) {
84
+ if (!file.startsWith("notes/"))
85
+ return deferred;
86
+ remaining -= (await readMemoryInventorySource(root, file, Math.min(remaining, inventoryLimits.fileBytes))).bytes;
87
+ }
88
+ return { enabled: false, deferred: true, legacyFallback: true };
89
+ }
90
+ if (Array.from(workspace.memory).length > LOCAL_MEMORY_TOTAL_CODE_POINT_CAP)
91
+ return deferred;
47
92
  const generated = /<!-- NOWWORK:MEMORY_INDEX:BEGIN schema=1 -->[\s\S]*?<!-- NOWWORK:MEMORY_INDEX:END -->/u.exec(memory.content)?.[0];
48
93
  if (!generated || Array.from(generated).length > LOCAL_MEMORY_INDEX_CODE_POINT_CAP)
49
94
  return deferred;
@@ -48,7 +48,10 @@ export function localMemoryResultPath(runDir, executionId) {
48
48
  const suffix = createHash("sha256").update(executionId, "utf8").digest("hex").slice(0, 32);
49
49
  return join(runDir, `.memory-result.v1-${suffix}.json`);
50
50
  }
51
- export function renderLocalMemoryResultInstructions(resultPath) {
51
+ export function renderLocalMemoryResultInstructions(resultPath, storageVersion = 1) {
52
+ const storagePolicy = storageVersion === 2
53
+ ? "完整目录保存在 MEMORY.md,模型只看到有界预览;未展示记录仍然有效。涉及旧规则时先用 crew-memory search 查询完整目录,按当前 ID/revision 更新,不因预览未命中就重复新建。20条预览不是有效记录上限;完整存储超出资源上限会拒绝提交,不得自行删除或降级旧记录。已有 pending 仍是未应用候选,须重新核对授权、重复与版本后通过 changes 申请保存,不因迁移自动生效。"
54
+ : "daemon 会校验、去重、限制 MEMORY.md 大小,超出索引容量的候选会保存在 pending 归档中。容量暂存的 create 可按需检索参考但不能覆盖现行规则;有空间的后续 proposed 提交可提升。未应用的 update/supersede 不进入普通召回,必须重新核对目标版本后处理。";
52
55
  return `
53
56
 
54
57
  ## Structured memory prune (daemon-managed)
@@ -70,7 +73,7 @@ outcome 只允许 no_change、proposed、conflict、failed。新增、更新、
70
73
  \`\`\`
71
74
 
72
75
  每个 changes 项须保留示例中的全部字段。operation=create|update|supersede;kind=procedural|semantic|episodic|preference;priority=high|medium|low;detailCategory=preferences|project-rules|testing|debugging|git|incidents|lessons。create 的 memoryId/baseRevision/supersedesMemoryId 必须为 null;update 按现有索引填写 memoryId/baseRevision,supersedesMemoryId 为 null;supersede 填 supersedesMemoryId/baseRevision,memoryId 为 null。
73
- 只提交可复用、与本次任务明确相关的少量候选;不要写入凭证、token、隐私或内部控制标记。daemon 会校验、去重、限制 MEMORY.md 大小,超出索引容量的候选会保存在 pending 归档中。容量暂存的 create 可按需检索参考但不能覆盖现行规则;有空间的后续 proposed 提交可提升。未应用的 update/supersede 不进入普通召回,必须重新核对目标版本后处理。
76
+ 只提交可复用、与本次任务明确相关的少量候选;不要写入凭证、token、隐私或内部控制标记。${storagePolicy}
74
77
  写入候选文件只表示提交候选,不能提前宣称已经记住或规则已经更新;daemon 在本轮结束后才处理,候选持久化与进入有效索引也不是同一结果。本结构化轮次覆盖前文提前清理工作日志的要求:保留 $CREW_TASK_LOG,不要清空或删除;daemon 将在处理成功后清理,失败时保留日志和候选用于排查或重试。
75
78
  `;
76
79
  }
@@ -0,0 +1,152 @@
1
+ import { LOCAL_MEMORY_INDEX_CODE_POINT_CAP, LOCAL_MEMORY_INDEX_ENTRY_CAP, LOCAL_MEMORY_TOTAL_CODE_POINT_CAP, renderGeneratedIndex, renderLocalMemoryIndexCard, selectLocalMemoryIndexEntries, } from "./index-budget.js";
2
+ export const LOCAL_MEMORY_STORAGE_BYTE_CAP = 16 * 1024 * 1024;
3
+ export const LOCAL_MEMORY_STORAGE_ENTRY_CAP = 10_000;
4
+ export const LOCAL_MEMORY_NOTE_BYTE_CAP = 32 * 1024 * 1024;
5
+ export const LOCAL_MEMORY_READ_BYTE_CAP = 64 * 1024 * 1024;
6
+ export const LOCAL_MEMORY_GENERATED_INDEX_PATTERN = /<!--\s*NOWWORK:MEMORY_INDEX:BEGIN\s+schema=[12]\s*-->[\s\S]*?<!--\s*NOWWORK:MEMORY_INDEX:END\s*-->/u;
7
+ const length = (text) => {
8
+ let count = 0;
9
+ for (const _character of text)
10
+ count += 1;
11
+ return count;
12
+ };
13
+ const TYPES = new Set(["procedural", "semantic", "episodic", "preference"]);
14
+ const STATUSES = new Set(["candidate", "active", "pinned", "stale", "superseded", "archived"]);
15
+ const PRIORITIES = new Set(["high", "medium", "low"]);
16
+ const FIELDS = new Set(["id", "file", "type", "status", "summary", "triggers", "priority", "revision", "anchor", "updated", "review_after"]);
17
+ /** A malformed/unknown marker is never treated as an unmarked legacy file. */
18
+ export function detectLocalMemoryStorageVersion(memory) {
19
+ const markerCount = (memory.match(/NOWWORK:MEMORY_INDEX:/gu) ?? []).length;
20
+ if (markerCount === 0)
21
+ return 0;
22
+ const block = LOCAL_MEMORY_GENERATED_INDEX_PATTERN.exec(memory);
23
+ if (markerCount !== 2 || block?.index === undefined)
24
+ throw new Error("local_memory_storage_version_invalid");
25
+ const headings = [...memory.matchAll(/^##[\t ]+Knowledge Index[\t ]*\r?$/gmu)];
26
+ const heading = headings[0];
27
+ if (headings.length !== 1 || heading?.index === undefined)
28
+ throw new Error("local_memory_storage_index_invalid");
29
+ const sectionStart = heading.index + heading[0].length;
30
+ const nextSection = /^##[\t ]+(?!#)/mu.exec(memory.slice(sectionStart));
31
+ const sectionEnd = nextSection?.index === undefined ? memory.length : sectionStart + nextSection.index;
32
+ if (block.index < sectionStart || block.index + block[0].length > sectionEnd) {
33
+ throw new Error("local_memory_storage_index_invalid");
34
+ }
35
+ return /schema=2\s*-->/u.test(block[0].slice(0, block[0].indexOf("\n") + 1) || block[0]) ? 2 : 1;
36
+ }
37
+ function validateEntry(entry) {
38
+ const singleLine = (value) => !/[\r\n\u0000-\u001f\u007f]/u.test(value)
39
+ && !/<!--/u.test(value);
40
+ if (!entry.title.trim() || !entry.summary.trim()
41
+ || ![entry.title, entry.summary, entry.updated, ...entry.triggers].every(singleLine)
42
+ || !/^[A-Za-z0-9_-]+$/u.test(entry.id) || entry.anchor !== entry.id
43
+ || !/^notes\/(?:preferences|project-rules|testing|debugging|git|incidents|lessons)\.md$/u.test(entry.file)
44
+ || !TYPES.has(entry.type) || !STATUSES.has(entry.status) || !PRIORITIES.has(entry.priority)
45
+ || !Number.isSafeInteger(entry.revision) || entry.revision < 1
46
+ || !entry.updated.trim()
47
+ || (entry.reviewAfter !== null && !singleLine(entry.reviewAfter))) {
48
+ throw new Error("local_memory_storage_entry_invalid");
49
+ }
50
+ }
51
+ function validateEntries(entries) {
52
+ if (entries.length > LOCAL_MEMORY_STORAGE_ENTRY_CAP)
53
+ throw new Error("local_memory_storage_entry_limit");
54
+ const ids = new Set();
55
+ for (const entry of entries) {
56
+ validateEntry(entry);
57
+ if (ids.has(entry.id))
58
+ throw new Error("local_memory_storage_duplicate_id");
59
+ ids.add(entry.id);
60
+ }
61
+ }
62
+ export function assertLocalMemoryStorageSize(memory) {
63
+ if (Buffer.byteLength(memory, "utf8") > LOCAL_MEMORY_STORAGE_BYTE_CAP)
64
+ throw new Error("local_memory_storage_byte_limit");
65
+ }
66
+ /** schema=2 stores every lifecycle state, unlike the schema=1 prompt projection. */
67
+ export function parseFullMemoryIndex(memory) {
68
+ assertLocalMemoryStorageSize(memory);
69
+ if (detectLocalMemoryStorageVersion(memory) !== 2)
70
+ throw new Error("local_memory_storage_version_invalid");
71
+ const block = LOCAL_MEMORY_GENERATED_INDEX_PATTERN.exec(memory)[0];
72
+ const body = block.replace(/^<!--[^>]*-->\s*/u, "").replace(/<!--[^>]*-->$/u, "");
73
+ const headings = [...body.matchAll(/^###[\t ]+(.+?)[\t ]*\r?$/gmu)];
74
+ if (headings.length > LOCAL_MEMORY_STORAGE_ENTRY_CAP)
75
+ throw new Error("local_memory_storage_entry_limit");
76
+ if (body.slice(0, headings[0]?.index ?? body.length).trim())
77
+ throw new Error("local_memory_storage_entry_invalid");
78
+ const entries = headings.map((heading, index) => {
79
+ const fields = {};
80
+ const tail = body.slice(heading.index + heading[0].length, headings[index + 1]?.index ?? body.length);
81
+ for (const line of tail.split(/\r?\n/u)) {
82
+ if (!line.trim())
83
+ continue;
84
+ const field = /^-\s+([a-z][a-z_]*)\s*:\s*(.*?)\s*$/u.exec(line);
85
+ if (!field || !FIELDS.has(field[1]) || fields[field[1]] !== undefined)
86
+ throw new Error("local_memory_storage_entry_invalid");
87
+ fields[field[1]] = field[2];
88
+ }
89
+ if ([...FIELDS].filter(field => field !== "review_after").some(field => fields[field] === undefined)) {
90
+ throw new Error("local_memory_storage_entry_invalid");
91
+ }
92
+ if (!/^[1-9][0-9]*$/u.test(fields.revision))
93
+ throw new Error("local_memory_storage_entry_invalid");
94
+ return {
95
+ id: fields.id, title: heading[1], file: fields.file, type: fields.type,
96
+ status: fields.status, summary: fields.summary,
97
+ triggers: fields.triggers.split(/[,,、]/u).map(value => value.trim()).filter(Boolean),
98
+ priority: fields.priority, revision: Number(fields.revision),
99
+ anchor: fields.anchor, updated: fields.updated, reviewAfter: fields.review_after ?? null,
100
+ };
101
+ });
102
+ validateEntries(entries);
103
+ const section = memory.slice(memory.indexOf("## Knowledge Index") + "## Knowledge Index".length);
104
+ const remaining = section.replace(LOCAL_MEMORY_GENERATED_INDEX_PATTERN, "").split(/^##[\t ]+(?!#)/mu)[0] ?? "";
105
+ if (/^###[\t ]+/mu.test(remaining.replace(/<!--[\s\S]*?-->/gu, "")))
106
+ throw new Error("local_memory_legacy_index_requires_migration");
107
+ return entries;
108
+ }
109
+ export function renderFullMemoryIndex(entries) {
110
+ validateEntries(entries);
111
+ const block = `<!-- NOWWORK:MEMORY_INDEX:BEGIN schema=2 -->\n${entries.map(renderLocalMemoryIndexCard).join("\n")}<!-- NOWWORK:MEMORY_INDEX:END -->`;
112
+ assertLocalMemoryStorageSize(block);
113
+ return block;
114
+ }
115
+ export function replaceFullMemoryIndex(memory, entries) {
116
+ if (detectLocalMemoryStorageVersion(memory) !== 2)
117
+ throw new Error("local_memory_storage_version_invalid");
118
+ const updated = memory.replace(LOCAL_MEMORY_GENERATED_INDEX_PATTERN, () => renderFullMemoryIndex(entries));
119
+ assertLocalMemoryStorageSize(updated);
120
+ return updated;
121
+ }
122
+ export function selectLocalMemoryPreview(memory, query = "", options = {}) {
123
+ const entries = parseFullMemoryIndex(memory);
124
+ const totalActiveEntries = entries.filter(entry => entry.status === "active" || entry.status === "pinned").length;
125
+ const emptyIndex = renderGeneratedIndex([]);
126
+ // Reserve the largest count label before selecting; all discovery text counts toward the same cap.
127
+ const hint = (shown) => `\nMemory preview: ${shown}/${totalActiveEntries} active entries. Use crew-memory search --query <text> and crew-memory read --file <file> for more.\n`;
128
+ const base = memory.replace(LOCAL_MEMORY_GENERATED_INDEX_PATTERN, () => `${emptyIndex}${hint(totalActiveEntries)}`);
129
+ const remaining = Math.min(LOCAL_MEMORY_TOTAL_CODE_POINT_CAP, options.maxCodePoints ?? Infinity) - length(base);
130
+ const remainingBytes = (options.maxBytes ?? Infinity) - Buffer.byteLength(base, "utf8");
131
+ if (remaining < 0 || remainingBytes < 0 || Number.isNaN(remaining) || Number.isNaN(remainingBytes)) {
132
+ throw new Error("local_memory_identity_budget_exceeded");
133
+ }
134
+ const terms = query.normalize("NFKC").toLowerCase().split(/\s+/u).filter(Boolean);
135
+ const selection = selectLocalMemoryIndexEntries(entries, {
136
+ entryCap: LOCAL_MEMORY_INDEX_ENTRY_CAP,
137
+ indexCodePointCap: Math.min(LOCAL_MEMORY_INDEX_CODE_POINT_CAP - length(emptyIndex), remaining),
138
+ indexByteCap: remainingBytes,
139
+ }, {
140
+ protectedIds: new Set(entries.filter(entry => entry.status === "pinned").map(entry => entry.id)),
141
+ relevance: entry => {
142
+ const searchable = `${entry.title} ${entry.summary} ${entry.triggers.join(" ")}`.normalize("NFKC").toLowerCase();
143
+ return terms.filter(term => searchable.includes(term)).length;
144
+ },
145
+ });
146
+ if (selection.reason === "index_over_budget_pinned")
147
+ throw new Error("local_memory_pinned_budget_exceeded");
148
+ return {
149
+ ...selection, totalActiveEntries,
150
+ memory: memory.replace(LOCAL_MEMORY_GENERATED_INDEX_PATTERN, () => `${renderGeneratedIndex(selection.entries)}${hint(selection.entries.length)}`),
151
+ };
152
+ }
@@ -6,6 +6,7 @@ import { dslog } from "../slog.js";
6
6
  import { LocalMemoryChangeSchema, LocalMemorySingleLineText, validateLocalMemoryChange, } from "./candidate.js";
7
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
+ import { assertLocalMemoryStorageSize, detectLocalMemoryStorageVersion, LOCAL_MEMORY_GENERATED_INDEX_PATTERN, LOCAL_MEMORY_NOTE_BYTE_CAP, LOCAL_MEMORY_READ_BYTE_CAP, parseFullMemoryIndex, replaceFullMemoryIndex, selectLocalMemoryPreview, } from "./storage-format.js";
9
10
  const StatusChangeSchema = z.object({
10
11
  memoryId: LocalMemorySingleLineText,
11
12
  expectedRevision: z.number().int().positive(),
@@ -90,12 +91,14 @@ function parseCards(section) {
90
91
  });
91
92
  }
92
93
  export function parseLocalMemoryIndex(memory) {
94
+ if (detectLocalMemoryStorageVersion(memory) === 2)
95
+ return parseFullMemoryIndex(memory);
93
96
  const section = indexSection(memory);
94
97
  const generated = GENERATED_INDEX_PATTERN.exec(section)?.[0];
95
98
  return parseCards(generated ?? section);
96
99
  }
97
100
  function parseUnmarkedMemoryIndex(memory) {
98
- const section = indexSection(memory).replace(GENERATED_INDEX_PATTERN, "");
101
+ const section = indexSection(memory).replace(LOCAL_MEMORY_GENERATED_INDEX_PATTERN, "");
99
102
  return parseCards(section);
100
103
  }
101
104
  function replaceGeneratedIndex(memory, entries) {
@@ -122,11 +125,36 @@ function noteSection(id, revision, title, body) {
122
125
  }
123
126
  function notePattern(id) {
124
127
  const escaped = id.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
125
- return new RegExp(`<!--\\s*NOWWORK:MEMORY_ENTRY:BEGIN\\s+id=${escaped}[^>]*-->[\\s\\S]*?<!--\\s*NOWWORK:MEMORY_ENTRY:END\\s+id=${escaped}\\s*-->\\s*`, "u");
128
+ return new RegExp(`<!--\\s*NOWWORK:MEMORY_ENTRY:BEGIN\\s+id=${escaped}(?=\\s|-->)[^>]*-->[\\s\\S]*?<!--\\s*NOWWORK:MEMORY_ENTRY:END\\s+id=${escaped}\\s*-->\\s*`, "u");
129
+ }
130
+ function uniqueNoteSection(content, id) {
131
+ const escaped = id.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
132
+ const begins = [...content.matchAll(new RegExp(`<!--\\s*NOWWORK:MEMORY_ENTRY:BEGIN\\s+id=${escaped}(?=\\s|-->)`, "gu"))];
133
+ const ends = [...content.matchAll(new RegExp(`<!--\\s*NOWWORK:MEMORY_ENTRY:END\\s+id=${escaped}\\s*-->`, "gu"))];
134
+ if (begins.length === 0 && ends.length === 0)
135
+ return null;
136
+ const section = notePattern(id).exec(content)?.[0];
137
+ if (begins.length !== 1 || ends.length !== 1 || section === undefined
138
+ || (section.match(/NOWWORK:MEMORY_ENTRY:(?:BEGIN|END)/gu) ?? []).length !== 2) {
139
+ throw new Error("local_memory_note_anchor_invalid");
140
+ }
141
+ return section;
126
142
  }
127
143
  function replaceNote(content, id, replacement) {
128
144
  const pattern = notePattern(id);
129
- return pattern.test(content) ? content.replace(pattern, replacement) : null;
145
+ return uniqueNoteSection(content, id) !== null ? content.replace(pattern, () => replacement) : null;
146
+ }
147
+ function updateNoteRevision(content, id, revision) {
148
+ const section = uniqueNoteSection(content, id);
149
+ const beginning = `<!-- NOWWORK:MEMORY_ENTRY:BEGIN id=${id} `;
150
+ const ending = `<!-- NOWWORK:MEMORY_ENTRY:END id=${id} -->`;
151
+ if (section === null || content.split(beginning).length !== 2 || content.split(ending).length !== 2) {
152
+ throw new Error("local_memory_note_anchor_missing");
153
+ }
154
+ const updated = section.replace(/^(<!--\s*NOWWORK:MEMORY_ENTRY:BEGIN\s+id=[A-Za-z0-9_-]+\s+revision=)[1-9][0-9]*(\s*-->)/u, (_match, prefix, suffix) => `${prefix}${revision}${suffix}`);
155
+ if (!updated.startsWith(`${beginning}revision=${revision} -->`))
156
+ throw new Error("local_memory_note_anchor_missing");
157
+ return content.replace(notePattern(id), () => updated);
130
158
  }
131
159
  function validateCandidate(result) {
132
160
  if (result.outcome === "no_change" && (result.changes.length > 0 || result.statusChanges.length > 0)) {
@@ -199,14 +227,14 @@ function detailCategoryFromFile(file) {
199
227
  return DETAIL_CATEGORIES.has(category) ? category : null;
200
228
  }
201
229
  function readNoteBody(content, id) {
202
- const begin = content.indexOf(`<!-- NOWWORK:MEMORY_ENTRY:BEGIN id=${id}`);
203
- if (begin < 0)
230
+ const section = uniqueNoteSection(content, id);
231
+ if (section === null)
204
232
  return null;
205
- const bodyStart = content.indexOf("\n\n", begin);
206
- const bodyEnd = content.indexOf(`\n<!-- NOWWORK:MEMORY_ENTRY:END id=${id}`, bodyStart + 2);
233
+ const bodyStart = section.indexOf("\n\n");
234
+ const bodyEnd = section.indexOf(`\n<!-- NOWWORK:MEMORY_ENTRY:END id=${id}`, bodyStart + 2);
207
235
  if (bodyStart < 0 || bodyEnd < 0)
208
236
  return null;
209
- return content.slice(bodyStart + 2, bodyEnd).trim();
237
+ return section.slice(bodyStart + 2, bodyEnd).trim();
210
238
  }
211
239
  function archivedChange(entry, detailBody) {
212
240
  const detailCategory = detailCategoryFromFile(entry.file);
@@ -249,7 +277,12 @@ async function readNote(path) {
249
277
  const stat = await lstat(path);
250
278
  if (!stat.isFile() || stat.isSymbolicLink())
251
279
  throw new Error("local_memory_note_path_invalid");
252
- return await readFile(path, "utf8");
280
+ if (stat.size > LOCAL_MEMORY_NOTE_BYTE_CAP)
281
+ throw new Error("local_memory_note_byte_limit");
282
+ const content = await readFile(path, "utf8");
283
+ if (Buffer.byteLength(content, "utf8") > LOCAL_MEMORY_NOTE_BYTE_CAP)
284
+ throw new Error("local_memory_note_byte_limit");
285
+ return content;
253
286
  }
254
287
  catch (error) {
255
288
  if (error.code === "ENOENT")
@@ -269,6 +302,11 @@ async function atomicWrite(path, content) {
269
302
  }
270
303
  }
271
304
  function projectIndex(memory, entries, protectedIds) {
305
+ if (detectLocalMemoryStorageVersion(memory) === 2) {
306
+ const updated = replaceFullMemoryIndex(memory, entries);
307
+ selectLocalMemoryPreview(updated);
308
+ return { memory: updated, omittedIds: [], reason: null };
309
+ }
272
310
  const empty = replaceGeneratedIndex(memory, []);
273
311
  if (codePointLength(empty) > LOCAL_MEMORY_TOTAL_CODE_POINT_CAP)
274
312
  throw new Error("local_memory_memory_over_budget");
@@ -311,7 +349,12 @@ export async function applyLocalMemoryResult(input) {
311
349
  const memoryPath = join(input.agentDir, "MEMORY.md");
312
350
  await assertWritablePaths(input.agentDir, memoryPath);
313
351
  return withApplyLease(input.agentDir, async () => {
352
+ const memoryStat = await lstat(memoryPath);
353
+ if (memoryStat.size > 16 * 1024 * 1024)
354
+ throw new Error("local_memory_storage_byte_limit");
314
355
  const memory = await readFile(memoryPath, "utf8");
356
+ assertLocalMemoryStorageSize(memory);
357
+ const fullStorage = detectLocalMemoryStorageVersion(memory) === 2;
315
358
  if (parseUnmarkedMemoryIndex(memory).length > 0)
316
359
  throw new Error("local_memory_legacy_index_requires_migration");
317
360
  let entries = [...parseLocalMemoryIndex(memory)];
@@ -342,6 +385,26 @@ export async function applyLocalMemoryResult(input) {
342
385
  const pendingCandidates = [];
343
386
  const resolvedPendingIds = new Set();
344
387
  const noteUpdates = new Map();
388
+ const noteCache = new Map();
389
+ let totalReadBytes = Buffer.byteLength(memory, "utf8");
390
+ const readStoredNote = async (path) => {
391
+ const cached = noteUpdates.get(path) ?? noteCache.get(path);
392
+ if (cached !== undefined)
393
+ return cached;
394
+ const stat = await lstat(path).catch((error) => {
395
+ if (error.code === "ENOENT")
396
+ return null;
397
+ throw error;
398
+ });
399
+ if (stat !== null && totalReadBytes + stat.size > LOCAL_MEMORY_READ_BYTE_CAP)
400
+ throw new Error("local_memory_read_byte_limit");
401
+ const content = await readNote(path);
402
+ totalReadBytes += Buffer.byteLength(content, "utf8");
403
+ if (totalReadBytes > LOCAL_MEMORY_READ_BYTE_CAP)
404
+ throw new Error("local_memory_read_byte_limit");
405
+ noteCache.set(path, content);
406
+ return content;
407
+ };
345
408
  let allowActiveEviction = false;
346
409
  const randomId = input.randomId ?? randomUUID;
347
410
  const executionId = input.executionId ?? "unknown";
@@ -383,10 +446,21 @@ export async function applyLocalMemoryResult(input) {
383
446
  if (existingCreate !== undefined && !matchesCreateCandidate(existingCreate, change)) {
384
447
  throw new Error("local_memory_duplicate_candidate");
385
448
  }
386
- if (existingCreate === undefined && entries.some((entry) => normalize(entry.title) === normalize(change.title)
387
- || normalize(entry.summary) === normalize(change.summary))) {
449
+ if (existingCreate === undefined && entries.some((entry) => (!fullStorage || entry.status === "active" || entry.status === "pinned")
450
+ && (normalize(entry.title) === normalize(change.title) || normalize(entry.summary) === normalize(change.summary)))) {
388
451
  throw new Error("local_memory_duplicate_candidate");
389
452
  }
453
+ if (existingCreate !== undefined) {
454
+ if (existingCreate.revision !== 1 || existingCreate.status !== "active") {
455
+ throw new Error("local_memory_create_replay_conflict");
456
+ }
457
+ const stored = await readStoredNote(resolve(input.agentDir, existingCreate.file));
458
+ if (uniqueNoteSection(stored, id)?.trimEnd() !== noteSection(id, 1, change.title, change.detailBody).trimEnd()) {
459
+ throw new Error("local_memory_create_replay_conflict");
460
+ }
461
+ // A proven duplicate is a no-op; it must never rewrite a newer body or status.
462
+ continue;
463
+ }
390
464
  }
391
465
  const revision = existingCreate?.revision ?? (current === undefined ? 1 : current.revision + 1);
392
466
  const entry = existingCreate ?? entryFromChange(change, id, revision, current?.status ?? "active");
@@ -400,7 +474,7 @@ export async function applyLocalMemoryResult(input) {
400
474
  : item),
401
475
  entry,
402
476
  ];
403
- const allowEvictionForChange = change.operation === "create" && change.priority === "high";
477
+ const allowEvictionForChange = !fullStorage && change.operation === "create" && change.priority === "high";
404
478
  const protectedIds = new Set(allowEvictionForChange ? pinnedIds(entries) : activeOrPinnedIds(entries));
405
479
  if (allowEvictionForChange)
406
480
  allowActiveEviction = true;
@@ -417,7 +491,7 @@ export async function applyLocalMemoryResult(input) {
417
491
  const notePath = resolve(input.agentDir, entry.file);
418
492
  if (!isWithin(resolve(input.agentDir, "notes"), notePath))
419
493
  throw new Error("local_memory_note_path_invalid");
420
- const previous = noteUpdates.get(notePath) ?? await readNote(notePath);
494
+ const previous = await readStoredNote(notePath);
421
495
  const replacement = noteSection(id, revision, change.title, change.detailBody);
422
496
  const existingSection = replaceNote(previous, id, replacement);
423
497
  const next = existingSection ?? (current === undefined
@@ -435,7 +509,7 @@ export async function applyLocalMemoryResult(input) {
435
509
  // Pending create candidates are durable overflow, not dead ends. Promote only
436
510
  // when the current catalog has room; supersede/update candidates stay pending
437
511
  // until a fresh revision-aware decision can safely replay them.
438
- const pendingCandidatesOnDisk = await readPendingMemoryCandidates({ agentDir: input.agentDir }).catch((error) => {
512
+ const pendingCandidatesOnDisk = fullStorage ? [] : await readPendingMemoryCandidates({ agentDir: input.agentDir }).catch((error) => {
439
513
  if (!(error instanceof Error) || error.message !== "memory_pending_directory_over_budget")
440
514
  throw error;
441
515
  // 自动提升是可选工作,目录查询预算不应阻断本轮已验证的更新或腾位操作。
@@ -460,7 +534,7 @@ export async function applyLocalMemoryResult(input) {
460
534
  || normalize(entry.summary) === normalize(change.summary)));
461
535
  if (duplicate !== undefined) {
462
536
  const duplicatePath = resolve(input.agentDir, duplicate.file);
463
- const duplicateBody = readNoteBody(noteUpdates.get(duplicatePath) ?? await readNote(duplicatePath), duplicate.id);
537
+ const duplicateBody = readNoteBody(await readStoredNote(duplicatePath), duplicate.id);
464
538
  if (duplicateBody === change.detailBody
465
539
  || duplicateBody === unwrappedNoteBody(change.title, change.detailBody).trim()) {
466
540
  resolvedPendingIds.add(pending.pendingId);
@@ -478,7 +552,7 @@ export async function applyLocalMemoryResult(input) {
478
552
  const notePath = resolve(input.agentDir, entry.file);
479
553
  if (!isWithin(resolve(input.agentDir, "notes"), notePath))
480
554
  throw new Error("local_memory_note_path_invalid");
481
- const previous = noteUpdates.get(notePath) ?? await readNote(notePath);
555
+ const previous = await readStoredNote(notePath);
482
556
  const replacement = noteSection(id, 1, change.title, change.detailBody);
483
557
  const existingSection = replaceNote(previous, id, replacement);
484
558
  const next = existingSection ?? `${previous.trimEnd()}${previous.trimEnd() ? "\n\n" : ""}${replacement}`;
@@ -487,6 +561,17 @@ export async function applyLocalMemoryResult(input) {
487
561
  createdIds.push(id);
488
562
  resolvedPendingIds.add(pending.pendingId);
489
563
  }
564
+ if (fullStorage) {
565
+ const changedMetadataIds = new Set([
566
+ ...statusChangedIds,
567
+ ...input.result.changes.flatMap(change => change.supersedesMemoryId === null ? [] : [change.supersedesMemoryId]),
568
+ ]);
569
+ for (const id of changedMetadataIds) {
570
+ const entry = entries.find(item => item.id === id);
571
+ const notePath = resolve(input.agentDir, entry.file);
572
+ noteUpdates.set(notePath, updateNoteRevision(await readStoredNote(notePath), id, entry.revision));
573
+ }
574
+ }
490
575
  let finalProjection = projectIndex(memory, entries, allowActiveEviction ? pinnedIds(entries) : activeOrPinnedIds(entries));
491
576
  if (finalProjection.reason !== null
492
577
  && (!allowActiveEviction || finalProjection.reason === "index_over_budget_pinned")) {
@@ -498,7 +583,7 @@ export async function applyLocalMemoryResult(input) {
498
583
  .filter((entry) => entry?.status === "active");
499
584
  for (const entry of omittedActiveEntries) {
500
585
  const notePath = resolve(input.agentDir, entry.file);
501
- const content = noteUpdates.get(notePath) ?? await readNote(notePath);
586
+ const content = await readStoredNote(notePath);
502
587
  const detailBody = readNoteBody(content, entry.id);
503
588
  const change = detailBody === null ? null : archivedChange(entry, detailBody);
504
589
  if (change === null)
@@ -519,6 +604,12 @@ export async function applyLocalMemoryResult(input) {
519
604
  if (finalProjection.reason !== null)
520
605
  throw new Error(finalProjection.reason);
521
606
  }
607
+ // Complete every resource check before the first persistent write.
608
+ assertLocalMemoryStorageSize(finalProjection.memory);
609
+ for (const content of noteUpdates.values()) {
610
+ if (Buffer.byteLength(content, "utf8") > LOCAL_MEMORY_NOTE_BYTE_CAP)
611
+ throw new Error("local_memory_note_byte_limit");
612
+ }
522
613
  for (const candidate of pendingCandidates)
523
614
  await persistPendingMemoryCandidate({ agentDir: input.agentDir, candidate });
524
615
  for (const [path, content] of noteUpdates)
@@ -25,6 +25,7 @@ import { RUNTIME_HEALTH_PROBE_CAPABILITY } from "./runtime-health.js";
25
25
  import { probeDeepSeekHarnessAcp, deepSeekHarnessLauncher } from "./runtimes/deepseek-harness.js";
26
26
  import { daemonHome } from "./computer-profile.js";
27
27
  import { daemonReleaseProvenance } from "./release-provenance.js";
28
+ import { MEMORY_MIGRATION_CAPABILITY, MEMORY_STORAGE_V2_CAPABILITY, MEMORY_STATE_GUARD_CAPABILITY } from "./memory-migration-protocol.js";
28
29
  export { executableRuntimes } from "./runtime-capabilities.js";
29
30
  const execFileP = promisify(execFile);
30
31
  export const PROJECT_WORKSPACES_CAPABILITY = "project_workspaces_v1";
@@ -39,6 +40,7 @@ export const NATIVE_PROJECT_WORKSPACE_READINESS = Object.freeze({
39
40
  });
40
41
  export const DAEMON_CAPABILITIES = Object.freeze([
41
42
  MARKETPLACE_CAPABILITY,
43
+ MEMORY_MIGRATION_CAPABILITY, MEMORY_STORAGE_V2_CAPABILITY, MEMORY_STATE_GUARD_CAPABILITY,
42
44
  "scheduled_job_v1",
43
45
  "reply_origin_v1",
44
46
  "origin_decision_v1",
@@ -73,6 +75,7 @@ export const daemonCapabilities = (runtimePlatform = process.platform, readiness
73
75
  return Object.freeze(DAEMON_CAPABILITIES.filter((capability) => ((capability !== MARKETPLACE_CAPABILITY || nativeWorkspacePlatform)
74
76
  && (capability !== PROJECT_WORKSPACES_CAPABILITY || nativeWorkspaceReady)
75
77
  && (capability !== "message_file_preview_v1" || nativeWorkspacePlatform)
78
+ && (nativeWorkspacePlatform || ![MEMORY_MIGRATION_CAPABILITY, MEMORY_STORAGE_V2_CAPABILITY, MEMORY_STATE_GUARD_CAPABILITY].includes(capability))
76
79
  && (nativeWorkspacePlatform
77
80
  || (capability !== "project_skills_v1"
78
81
  && capability !== "project_skill_projection_v2"
package/dist/main.js CHANGED
File without changes
@@ -0,0 +1,84 @@
1
+ import { z } from "zod";
2
+ import { createRuntimeCancellation } from "./runtime-cancellation.js";
3
+ import { MemoryMigrationStartSchema } from "./memory-migration-protocol.js";
4
+ import { runMemoryMigration } from "./memory-migration-runtime.js";
5
+ import { inspectMemoryForMigration } from "./memory-migration-inspect.js";
6
+ const Inspect = MemoryMigrationStartSchema.pick({ reqId: true, protocolVersion: true, workspaceId: true,
7
+ agentId: true, handle: true, machineId: true }).extend({ type: z.literal("memory:migration:inspect"), executionId: z.string().uuid().optional() }).strict();
8
+ export function createMemoryMigrationController(input) {
9
+ let identity = null;
10
+ const runs = new Map();
11
+ return {
12
+ setIdentity(value) { identity = value; },
13
+ cancel(executionId) { const run = runs.get(executionId); if (!run)
14
+ return false; run.cancel(); return true; },
15
+ async handle(raw) {
16
+ const type = raw?.type;
17
+ if (type !== "memory:migration:start" && type !== "memory:migration:inspect")
18
+ return false;
19
+ const parsed = type === "memory:migration:start" ? MemoryMigrationStartSchema.safeParse(raw) : Inspect.safeParse(raw);
20
+ if (!parsed.success) {
21
+ const reqId = raw.reqId;
22
+ if (typeof reqId === "string" && reqId.length <= 200)
23
+ input.send({ type: "fs:result", reqId, ok: false, error: "memory_migration_invalid_request" });
24
+ return true;
25
+ }
26
+ const request = parsed.data;
27
+ if (!identity || request.workspaceId !== identity.workspaceId || request.machineId !== identity.machineId) {
28
+ input.send({ type: "fs:result", reqId: request.reqId, ok: false, error: "memory_migration_identity_mismatch" });
29
+ return true;
30
+ }
31
+ let journal;
32
+ try {
33
+ journal = input.journal();
34
+ }
35
+ catch {
36
+ input.send({ type: "fs:result", reqId: request.reqId, ok: false, error: "memory_migration_journal_unavailable" });
37
+ return true;
38
+ }
39
+ if (request.type === "memory:migration:inspect") {
40
+ try {
41
+ const data = await inspectMemoryForMigration(input.config.agentsRoot, request, journal);
42
+ input.send({ type: "fs:result", reqId: request.reqId, ok: true, data });
43
+ }
44
+ catch {
45
+ input.send({ type: "fs:result", reqId: request.reqId, ok: false, error: "memory_migration_inspection_failed" });
46
+ }
47
+ return true;
48
+ }
49
+ const existing = runs.get(request.executionId);
50
+ if (existing) {
51
+ input.send({ type: "fs:result", reqId: request.reqId, ok: true, data: { kind: "running" } });
52
+ return true;
53
+ }
54
+ if (input.isAdmissionClosed?.()) {
55
+ input.send({ type: "fs:result", reqId: request.reqId, ok: false, error: "memory_migration_storage_busy" });
56
+ return true;
57
+ }
58
+ const cancellation = createRuntimeCancellation();
59
+ const promise = runMemoryMigration(request, {
60
+ agentsRoot: input.config.agentsRoot, coordinator: input.coordinator, journal,
61
+ cancellation: cancellation.cancellation, send: input.send,
62
+ control: async (step, body) => {
63
+ const base = `/daemon/memory-migrations/${encodeURIComponent(request.agentId)}`;
64
+ const path = step === "history" ? `${base}/history` : `${base}/${request.migrationId}/${step === "needs-review" ? "review" : step}`;
65
+ const response = await fetch(new URL(path, input.config.serverUrl), {
66
+ method: step === "history" ? "GET" : "POST", signal: AbortSignal.timeout(10_000),
67
+ headers: { authorization: `Bearer ${input.config.machineToken}`, "content-type": "application/json" },
68
+ ...(step === "history" ? {} : { body: JSON.stringify(body) }),
69
+ });
70
+ const result = await response.json();
71
+ if (!response.ok || result.success !== true)
72
+ throw new Error("migration_control_unconfirmed");
73
+ return result.data;
74
+ },
75
+ }).then(data => input.send({ type: "fs:result", reqId: request.reqId, ok: true, data }))
76
+ .catch(() => input.send({ type: "fs:result", reqId: request.reqId, ok: false, error: "memory_migration_failed" }))
77
+ .finally(() => runs.delete(request.executionId));
78
+ runs.set(request.executionId, { promise, cancel: cancellation.request });
79
+ return true;
80
+ },
81
+ async stop() { for (const run of runs.values())
82
+ run.cancel(); await Promise.allSettled([...runs.values()].map(run => run.promise)); },
83
+ };
84
+ }